mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-27 23:12:49 -05:00
Compare commits
213 Commits
displaycon
...
c1d95a3086
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1d95a3086 | ||
|
|
9b027df1d5 | ||
|
|
5e03afe7f0 | ||
|
|
145a974b6d | ||
|
|
d23fc9f2df | ||
|
|
7ac5191e8d | ||
|
|
29d27ebd6d | ||
|
|
e45075dd84 | ||
|
|
80bc87e76b | ||
|
|
76d88517ec | ||
|
|
151d695212 | ||
|
|
2e1bed5fb5 | ||
|
|
f163b97c17 | ||
|
|
436c99927e | ||
|
|
aa72eacae7 | ||
|
|
913bb2ff67 | ||
|
|
3bb2696263 | ||
|
|
166843ded4 | ||
|
|
02166a4ca5 | ||
|
|
f0f2e6ef72 | ||
|
|
8d8d5de5fd | ||
|
|
6d76f0b476 | ||
|
|
f3f720bb37 | ||
|
|
2bf85bc4dd | ||
|
|
faddc46185 | ||
|
|
2991aac82e | ||
|
|
e1817027b1 | ||
|
|
ba2d51bcbb | ||
|
|
7f10d6a9b8 | ||
|
|
405749aa98 | ||
|
|
77681fd387 | ||
|
|
8253ec4496 | ||
|
|
a1e001e640 | ||
|
|
3a65ea21ba | ||
|
|
7d761c4c9a | ||
|
|
4cb90c5367 | ||
|
|
1c7d15db0b | ||
|
|
7268a3fe7f | ||
|
|
d2c4391514 | ||
|
|
69b1d0c2da | ||
|
|
ba28767492 | ||
|
|
6cff5f1146 | ||
|
|
3e1c6534bd | ||
|
|
c1d57946d9 | ||
|
|
5e111d89a5 | ||
|
|
1a98da22b2 | ||
|
|
618ccbcb2f | ||
|
|
d3a79a055e | ||
|
|
bae32e51ff | ||
|
|
edfda965e9 | ||
|
|
a547966b23 | ||
|
|
f6279b1b2e | ||
|
|
957c89a85d | ||
|
|
571a9dabcd | ||
|
|
51ca9a7686 | ||
|
|
c141ad1e34 | ||
|
|
37f972d075 | ||
|
|
7d8de6e6f0 | ||
|
|
7ff751f8a2 | ||
|
|
651672afe2 | ||
|
|
2dbadfe1b5 | ||
|
|
621710bd86 | ||
|
|
1edecb05bb | ||
|
|
f1a876301b | ||
|
|
97a07c399a | ||
|
|
18f095cb23 | ||
|
|
d95d516d64 | ||
|
|
45ba64ab02 | ||
|
|
9501d66af6 | ||
|
|
2127fc339a | ||
|
|
7962fee0bd | ||
|
|
d5c7b5c0cc | ||
|
|
5f77d69dd8 | ||
|
|
60034be06a | ||
|
|
518a5d38aa | ||
|
|
2eeaf8ff62 | ||
|
|
cffee0fae6 | ||
|
|
f08e2ef5b8 | ||
|
|
2b0070c31a | ||
|
|
ae82716afa | ||
|
|
c281bf3b53 | ||
|
|
45b8b2a895 | ||
|
|
7b9ba840fb | ||
|
|
84fb567ff5 | ||
|
|
bbd57e0673 | ||
|
|
b1632a0a03 | ||
|
|
7aff1182c8 | ||
|
|
fbe362cd20 | ||
|
|
e7f94c94cc | ||
|
|
7523190b16 | ||
|
|
da37e16b6e | ||
|
|
5c420ab50b | ||
|
|
4493b7c231 | ||
|
|
40a96c6eaf | ||
|
|
23a93082c6 | ||
|
|
f7650b5e1f | ||
|
|
3ebdd5631c | ||
|
|
6c4caf121a | ||
|
|
89788e9ca7 | ||
|
|
0787c63fed | ||
|
|
9fc0d5efff | ||
|
|
6611dfbe05 | ||
|
|
8a71ead51d | ||
|
|
d9d6ab5776 | ||
|
|
d6fe7bea27 | ||
|
|
1194f3ffb8 | ||
|
|
5ac81e6dd6 | ||
|
|
987856a1de | ||
|
|
ef52ce0990 | ||
|
|
06b14a5869 | ||
|
|
fd839059c0 | ||
|
|
ec6db7962a | ||
|
|
adf92cbc46 | ||
|
|
6b6f51cd1f | ||
|
|
df6c60213f | ||
|
|
6303304a10 | ||
|
|
8e76789119 | ||
|
|
10e81cfdd3 | ||
|
|
03fd3a4f16 | ||
|
|
8fdc748ed2 | ||
|
|
6c56d23b93 | ||
|
|
45d34dcb5b | ||
|
|
d7ac0d50fa | ||
|
|
1d4d145187 | ||
|
|
a5b9ff98c0 | ||
|
|
6feaecd92e | ||
|
|
b066a25308 | ||
|
|
777a552b57 | ||
|
|
7dbe608c28 | ||
|
|
61630e447b | ||
|
|
91385e7c83 | ||
|
|
04648fcca7 | ||
|
|
080fc7e44e | ||
|
|
0b60da3d6d | ||
|
|
a4492b90e7 | ||
|
|
c9331b7338 | ||
|
|
4982ea53dd | ||
|
|
c703cb6504 | ||
|
|
a7494971fd | ||
|
|
c548255bfc | ||
|
|
9656c7afd7 | ||
|
|
414b8c8272 | ||
|
|
b4f83d09d4 | ||
|
|
67ee74ac20 | ||
|
|
93539d2b6b | ||
|
|
524d967745 | ||
|
|
0effbebbb6 | ||
|
|
dca07a70f8 | ||
|
|
02936c97fd | ||
|
|
8f7e732827 | ||
|
|
5ffe563b7d | ||
|
|
6ef08c3d54 | ||
|
|
908b4b58cd | ||
|
|
f2611e0de0 | ||
|
|
ea75a9d351 | ||
|
|
3a744d7d68 | ||
|
|
195d312ae2 | ||
|
|
76006a7377 | ||
|
|
11536da512 | ||
|
|
2a91bc41f7 | ||
|
|
baf23157fc | ||
|
|
83b81be825 | ||
|
|
4aefa0f1f7 | ||
|
|
e53a7cee97 | ||
|
|
8437e1aa7b | ||
|
|
632f40cc0a | ||
|
|
7d81445341 | ||
|
|
78a5f401d7 | ||
|
|
8745f98c95 | ||
|
|
f0f5bcc630 | ||
|
|
8a3c513605 | ||
|
|
145d2636dd | ||
|
|
f2b9dc8988 | ||
|
|
2e4d56728b | ||
|
|
18231ed324 | ||
|
|
d0b61d8ed1 | ||
|
|
d385a44949 | ||
|
|
d97392d46e | ||
|
|
6abb2c73fd | ||
|
|
7e141c6b36 | ||
|
|
53553c1f62 | ||
|
|
523ccc6bf8 | ||
|
|
811e89fcfa | ||
|
|
5d5be4d9d7 | ||
|
|
88457ab139 | ||
|
|
0034926df7 | ||
|
|
d082d41ab9 | ||
|
|
b7911475b6 | ||
|
|
672754b0b5 | ||
|
|
0d1553123b | ||
|
|
ba6c51c102 | ||
|
|
d64206a9ff | ||
|
|
d9a1089039 | ||
|
|
55fe463405 | ||
|
|
e84210e962 | ||
|
|
ff506548d3 | ||
|
|
f6b09751e9 | ||
|
|
3d863979c4 | ||
|
|
2947ff4131 | ||
|
|
b8fca10896 | ||
|
|
33e45794d2 | ||
|
|
42cc88ca65 | ||
|
|
0b7f2416ca | ||
|
|
5d5c745ee5 | ||
|
|
e0429e4c60 | ||
|
|
0bece5287e | ||
|
|
60b5e47836 | ||
|
|
aa75b44790 | ||
|
|
769f58caa9 | ||
|
|
e7facf740d | ||
|
|
04921eef62 | ||
|
|
8863c42879 | ||
|
|
2745116ac5 |
65
.github/ISSUE_TEMPLATE/bug_report.md
vendored
65
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,65 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug Report
|
|
||||||
about: Crashes or unexpected behaviors
|
|
||||||
title: ""
|
|
||||||
labels: "bug"
|
|
||||||
assignees: ""
|
|
||||||
---
|
|
||||||
|
|
||||||
<!-- If your issue is related to ICONS
|
|
||||||
- Purple and black checkerboards are QT's way of signalling an icon doesn't exist
|
|
||||||
- FIX: Configure a QT6 or Icon Pack in DMS Settings that has the icon you want
|
|
||||||
- Follow the [THEMING](https://danklinux.com/docs/dankmaterialshell/icon-theming) section to ensure your QT environment variable is configured correctly for themes.
|
|
||||||
- Once done, configure an icon theme - either however you normally do with gtk3 or qt6ct, or through the built-in settings modal. -->
|
|
||||||
|
|
||||||
## Compositor
|
|
||||||
|
|
||||||
- [ ] niri
|
|
||||||
- [ ] Hyprland
|
|
||||||
- [ ] dwl (MangoWC)
|
|
||||||
- [ ] sway
|
|
||||||
- [ ] Other (specify)
|
|
||||||
|
|
||||||
## Distribution
|
|
||||||
|
|
||||||
<!-- Arch, Fedora, Debian, etc. -->
|
|
||||||
|
|
||||||
## dms version
|
|
||||||
|
|
||||||
<!-- Output of dms version command -->
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- Brief description of the issue -->
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
<!-- Describe what you expected to happen -->
|
|
||||||
|
|
||||||
## Steps to Reproduce
|
|
||||||
|
|
||||||
<!-- Please provide detailed steps to reproduce the issue -->
|
|
||||||
|
|
||||||
1.
|
|
||||||
2.
|
|
||||||
3.
|
|
||||||
|
|
||||||
## Error Messages/Logs
|
|
||||||
|
|
||||||
<!-- Please include any error messages, stack traces, or relevant logs -->
|
|
||||||
<!-- you can get a log file with the following steps:
|
|
||||||
dms kill
|
|
||||||
mkdir ~/dms_logs
|
|
||||||
nohup dms run > ~/dms_logs/dms-$(date +%s).txt 2>&1 &
|
|
||||||
|
|
||||||
Then trigger your issue, and share the contents of ~/dms_logs/dms-<timestamp>.txt
|
|
||||||
|
|
||||||
-->
|
|
||||||
|
|
||||||
```
|
|
||||||
Paste error messages or logs here
|
|
||||||
```
|
|
||||||
|
|
||||||
## Screenshots/Recordings
|
|
||||||
|
|
||||||
<!-- If applicable, add screenshots or screen recordings -->
|
|
||||||
96
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
96
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Crashes or unexpected behaviors
|
||||||
|
labels:
|
||||||
|
- bug
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## DankMaterialShell Bug Report
|
||||||
|
Limit your report to one issue per submission unless closely related
|
||||||
|
- type: checkboxes
|
||||||
|
id: compositor
|
||||||
|
attributes:
|
||||||
|
label: Compositor
|
||||||
|
options:
|
||||||
|
- label: Niri
|
||||||
|
- label: Hyprland
|
||||||
|
- label: MangoWC (dwl)
|
||||||
|
- label: Sway
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: checkboxes
|
||||||
|
id: distribution
|
||||||
|
attributes:
|
||||||
|
label: Distribution
|
||||||
|
options:
|
||||||
|
- label: Arch Linux
|
||||||
|
- label: CachyOS
|
||||||
|
- label: Fedora
|
||||||
|
- label: NixOS
|
||||||
|
- label: Debian
|
||||||
|
- label: Ubuntu
|
||||||
|
- label: Gentoo
|
||||||
|
- label: OpenSUSE
|
||||||
|
- label: Other (specify below)
|
||||||
|
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: input
|
||||||
|
id: dms_version
|
||||||
|
attributes:
|
||||||
|
label: dms version
|
||||||
|
description: Output of dms version command
|
||||||
|
placeholder: e.g., 1.2.3
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: Brief description of the issue
|
||||||
|
placeholder: What happened?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: expected_behavior
|
||||||
|
attributes:
|
||||||
|
label: Expected Behavior
|
||||||
|
description: What did you expect to happen?
|
||||||
|
placeholder: Describe the expected behavior
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: steps_to_reproduce
|
||||||
|
attributes:
|
||||||
|
label: Steps to Reproduce & Installation Method
|
||||||
|
description: Please provide detailed steps to reproduce the issue
|
||||||
|
placeholder: |
|
||||||
|
1. ...
|
||||||
|
2. ...
|
||||||
|
3. ...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Error Messages/Logs
|
||||||
|
description: Please include any error messages, stack traces, or relevant logs
|
||||||
|
placeholder: |
|
||||||
|
Paste error messages or logs here
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: screenshots
|
||||||
|
attributes:
|
||||||
|
label: Screenshots/Recordings
|
||||||
|
description: If applicable, add screenshots or screen recordings
|
||||||
|
placeholder: Attach images or videos here
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
33
.github/ISSUE_TEMPLATE/feature_request.md
vendored
33
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,33 +0,0 @@
|
|||||||
---
|
|
||||||
name: Request a Feature
|
|
||||||
about: New widgets, new widget behavior, etc.
|
|
||||||
title: ""
|
|
||||||
labels: "enhancement"
|
|
||||||
assignees: ""
|
|
||||||
---
|
|
||||||
|
|
||||||
## Feature Description
|
|
||||||
|
|
||||||
<!-- Brief description of the feature requested -->
|
|
||||||
|
|
||||||
## Use Case
|
|
||||||
|
|
||||||
<!-- Explain the purpose of this feature/why it'd be useful to you -->
|
|
||||||
|
|
||||||
## Compositor
|
|
||||||
|
|
||||||
Is this feature specific to one compositor?
|
|
||||||
|
|
||||||
- [ ] All compositors
|
|
||||||
- [ ] niri
|
|
||||||
- [ ] Hyprland
|
|
||||||
- [ ] dwl (MangoWC)
|
|
||||||
- [ ] sway
|
|
||||||
|
|
||||||
## Proposed Solution
|
|
||||||
|
|
||||||
<!-- If you have any ideas for how to implement this, please share! -->
|
|
||||||
|
|
||||||
## Alternatives/Existing Solutions
|
|
||||||
|
|
||||||
<!-- Include any similar/pre-existing products that solve this problem -->
|
|
||||||
55
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
55
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Suggest a new feature or improvement for DMS
|
||||||
|
labels:
|
||||||
|
- enhancement
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## DankMaterialShell Feature Request
|
||||||
|
- type: textarea
|
||||||
|
id: feature_description
|
||||||
|
attributes:
|
||||||
|
label: Feature Description
|
||||||
|
description: Brief description of the feature requested
|
||||||
|
placeholder: What feature would you like to see?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: use_case
|
||||||
|
attributes:
|
||||||
|
label: Use Case
|
||||||
|
description: Explain the purpose of this feature/why it'd be useful to you
|
||||||
|
placeholder: Why is this feature important?
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: checkboxes
|
||||||
|
id: compositor
|
||||||
|
attributes:
|
||||||
|
label: Compositor(s)
|
||||||
|
description: Is this feature specific to one or more compositors?
|
||||||
|
options:
|
||||||
|
- label: All compositors
|
||||||
|
- label: Niri
|
||||||
|
- label: Hyprland
|
||||||
|
- label: MangoWC (dwl)
|
||||||
|
- label: Sway
|
||||||
|
- label: Other (specify below)
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: proposed_solution
|
||||||
|
attributes:
|
||||||
|
label: Proposed Solution
|
||||||
|
description: If you have any ideas for how to implement this, please share!
|
||||||
|
placeholder: Suggest a solution or approach
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Alternatives/Existing Solutions
|
||||||
|
description: Include any similar/pre-existing products that solve this problem
|
||||||
|
placeholder: List alternatives or existing solutions
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
40
.github/ISSUE_TEMPLATE/support_request.md
vendored
40
.github/ISSUE_TEMPLATE/support_request.md
vendored
@@ -1,40 +0,0 @@
|
|||||||
---
|
|
||||||
name: Request Assistance or Support
|
|
||||||
about: Help with installation, usage, or general questions.
|
|
||||||
title: ""
|
|
||||||
labels: "support"
|
|
||||||
assignees: ""
|
|
||||||
---
|
|
||||||
|
|
||||||
## Compositor
|
|
||||||
|
|
||||||
- [ ] niri
|
|
||||||
- [ ] Hyprland
|
|
||||||
- [ ] dwl (MangoWC)
|
|
||||||
- [ ] sway
|
|
||||||
- [ ] other
|
|
||||||
|
|
||||||
## Distribution
|
|
||||||
|
|
||||||
<!-- Arch, Fedora, Debian, etc. -->
|
|
||||||
|
|
||||||
## dms version
|
|
||||||
|
|
||||||
<!-- Output of dms version command -->
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- Brief description of the support needed -->
|
|
||||||
|
|
||||||
## Solutions Tried
|
|
||||||
|
|
||||||
<!-- Describe what you've tried so far -->
|
|
||||||
<!-- Outlining what you've tried so far helps us make improvements to the user experience and documentation to avoid recurrent issues -->
|
|
||||||
|
|
||||||
## Configuration Details
|
|
||||||
|
|
||||||
<!-- Include any configuration if relevant -->
|
|
||||||
|
|
||||||
## Screenshots/Recordings
|
|
||||||
|
|
||||||
<!-- If applicable, add screenshots or screen recordings -->
|
|
||||||
69
.github/ISSUE_TEMPLATE/support_request.yml
vendored
Normal file
69
.github/ISSUE_TEMPLATE/support_request.yml
vendored
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
name: Support Request
|
||||||
|
description: Help with installation, usage, or general questions about DankMaterialShell
|
||||||
|
labels:
|
||||||
|
- support
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## DankMaterialShell Support Request
|
||||||
|
- type: checkboxes
|
||||||
|
id: compositor
|
||||||
|
attributes:
|
||||||
|
label: Compositor
|
||||||
|
options:
|
||||||
|
- label: Niri
|
||||||
|
- label: Hyprland
|
||||||
|
- label: MangoWC (dwl)
|
||||||
|
- label: Sway
|
||||||
|
- label: Other (specify below)
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: input
|
||||||
|
id: distribution
|
||||||
|
attributes:
|
||||||
|
label: Distribution
|
||||||
|
description: Which Linux distribution are you using? (e.g., Arch, Fedora, Debian, etc.)
|
||||||
|
placeholder: Your Linux distribution
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: input
|
||||||
|
id: dms_version
|
||||||
|
attributes:
|
||||||
|
label: dms version
|
||||||
|
description: Output of dms version command
|
||||||
|
placeholder: e.g., 1.2.3
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: Brief description of the support needed
|
||||||
|
placeholder: What do you need help with?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: solutions_tried
|
||||||
|
attributes:
|
||||||
|
label: Solutions Tried
|
||||||
|
description: Describe what you've tried so far (commands, documentation, etc.)
|
||||||
|
placeholder: List steps or resources you've already tried
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: configuration
|
||||||
|
attributes:
|
||||||
|
label: Configuration Details
|
||||||
|
description: Include any relevant configuration if relevant
|
||||||
|
placeholder: Add configuration or environment info
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: screenshots
|
||||||
|
attributes:
|
||||||
|
label: Screenshots/Recordings
|
||||||
|
description: If applicable, add screenshots or screen recordings
|
||||||
|
placeholder: Attach images or videos here
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
9
.github/workflows/go-ci.yml
vendored
9
.github/workflows/go-ci.yml
vendored
@@ -28,6 +28,15 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install flatpak
|
||||||
|
run: sudo apt update && sudo apt install -y flatpak
|
||||||
|
|
||||||
|
- name: Add flathub
|
||||||
|
run: sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||||
|
|
||||||
|
- name: Add a flatpak that mutagen could support
|
||||||
|
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
|
|||||||
9
.github/workflows/prek.yml
vendored
9
.github/workflows/prek.yml
vendored
@@ -11,5 +11,14 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install flatpak
|
||||||
|
run: sudo apt update && sudo apt install -y flatpak
|
||||||
|
|
||||||
|
- name: Add flathub
|
||||||
|
run: sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||||
|
|
||||||
|
- name: Add a flatpak that mutagen could support
|
||||||
|
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
||||||
|
|
||||||
- name: run pre-commit hooks
|
- name: run pre-commit hooks
|
||||||
uses: j178/prek-action@v1
|
uses: j178/prek-action@v1
|
||||||
|
|||||||
294
.github/workflows/release.yml
vendored
294
.github/workflows/release.yml
vendored
@@ -398,297 +398,3 @@ jobs:
|
|||||||
prerelease: ${{ contains(env.TAG, '-') }}
|
prerelease: ${{ contains(env.TAG, '-') }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
# trigger-obs-update:
|
|
||||||
# runs-on: ubuntu-latest
|
|
||||||
# needs: release
|
|
||||||
# env:
|
|
||||||
# TAG: ${{ inputs.tag }}
|
|
||||||
# steps:
|
|
||||||
# - name: Checkout
|
|
||||||
# uses: actions/checkout@v4
|
|
||||||
# with:
|
|
||||||
# ref: ${{ inputs.tag }}
|
|
||||||
|
|
||||||
# - name: Install OSC
|
|
||||||
# run: |
|
|
||||||
# sudo apt-get update
|
|
||||||
# sudo apt-get install -y osc
|
|
||||||
|
|
||||||
# mkdir -p ~/.config/osc
|
|
||||||
# cat > ~/.config/osc/oscrc << EOF
|
|
||||||
# [general]
|
|
||||||
# apiurl = https://api.opensuse.org
|
|
||||||
|
|
||||||
# [https://api.opensuse.org]
|
|
||||||
# user = ${{ secrets.OBS_USERNAME }}
|
|
||||||
# pass = ${{ secrets.OBS_PASSWORD }}
|
|
||||||
# EOF
|
|
||||||
# chmod 600 ~/.config/osc/oscrc
|
|
||||||
|
|
||||||
# - name: Update OBS packages
|
|
||||||
# run: |
|
|
||||||
# cd distro
|
|
||||||
# bash scripts/obs-upload.sh dms "Update to ${TAG}"
|
|
||||||
|
|
||||||
# trigger-ppa-update:
|
|
||||||
# runs-on: ubuntu-latest
|
|
||||||
# needs: release
|
|
||||||
# env:
|
|
||||||
# TAG: ${{ inputs.tag }}
|
|
||||||
# steps:
|
|
||||||
# - name: Checkout
|
|
||||||
# uses: actions/checkout@v4
|
|
||||||
# with:
|
|
||||||
# ref: ${{ inputs.tag }}
|
|
||||||
|
|
||||||
# - name: Install build dependencies
|
|
||||||
# run: |
|
|
||||||
# sudo apt-get update
|
|
||||||
# sudo apt-get install -y \
|
|
||||||
# debhelper \
|
|
||||||
# devscripts \
|
|
||||||
# dput \
|
|
||||||
# lftp \
|
|
||||||
# build-essential \
|
|
||||||
# fakeroot \
|
|
||||||
# dpkg-dev
|
|
||||||
|
|
||||||
# - name: Configure GPG
|
|
||||||
# env:
|
|
||||||
# GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
|
||||||
# run: |
|
|
||||||
# echo "$GPG_KEY" | gpg --import
|
|
||||||
# GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
|
|
||||||
# echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
# - name: Upload to PPA
|
|
||||||
# run: |
|
|
||||||
# cd distro/ubuntu/ppa
|
|
||||||
# bash create-and-upload.sh ../dms dms questing
|
|
||||||
|
|
||||||
# copr-build:
|
|
||||||
# runs-on: ubuntu-latest
|
|
||||||
# needs: release
|
|
||||||
# env:
|
|
||||||
# TAG: ${{ inputs.tag }}
|
|
||||||
|
|
||||||
# steps:
|
|
||||||
# - name: Checkout repository
|
|
||||||
# uses: actions/checkout@v4
|
|
||||||
# with:
|
|
||||||
# ref: ${{ inputs.tag }}
|
|
||||||
|
|
||||||
# - name: Determine version
|
|
||||||
# id: version
|
|
||||||
# run: |
|
|
||||||
# VERSION="${TAG#v}"
|
|
||||||
# echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
# echo "Building DMS stable version: $VERSION"
|
|
||||||
|
|
||||||
# - name: Setup build environment
|
|
||||||
# run: |
|
|
||||||
# sudo apt-get update
|
|
||||||
# sudo apt-get install -y rpm wget curl jq gzip
|
|
||||||
# mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
|
||||||
|
|
||||||
# - name: Download release assets
|
|
||||||
# run: |
|
|
||||||
# VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
# cd ~/rpmbuild/SOURCES
|
|
||||||
|
|
||||||
# wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
|
|
||||||
# echo "Failed to download dms-qml.tar.gz for v${VERSION}"
|
|
||||||
# exit 1
|
|
||||||
# }
|
|
||||||
|
|
||||||
# - name: Generate stable spec file
|
|
||||||
# run: |
|
|
||||||
# VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
# CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
|
||||||
|
|
||||||
# cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
|
|
||||||
# # Spec for DMS stable releases - Generated by GitHub Actions
|
|
||||||
|
|
||||||
# %global debug_package %{nil}
|
|
||||||
# %global version VERSION_PLACEHOLDER
|
|
||||||
# %global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
|
||||||
|
|
||||||
# Name: dms
|
|
||||||
# Version: %{version}
|
|
||||||
# Release: 1%{?dist}
|
|
||||||
# Summary: %{pkg_summary}
|
|
||||||
|
|
||||||
# License: MIT
|
|
||||||
# URL: https://github.com/AvengeMedia/DankMaterialShell
|
|
||||||
|
|
||||||
# Source0: dms-qml.tar.gz
|
|
||||||
|
|
||||||
# BuildRequires: gzip
|
|
||||||
# BuildRequires: wget
|
|
||||||
# BuildRequires: systemd-rpm-macros
|
|
||||||
|
|
||||||
# Requires: (quickshell or quickshell-git)
|
|
||||||
# Requires: accountsservice
|
|
||||||
# Requires: dms-cli = %{version}-%{release}
|
|
||||||
# Requires: dgop
|
|
||||||
|
|
||||||
# Recommends: cava
|
|
||||||
# Recommends: cliphist
|
|
||||||
# Recommends: danksearch
|
|
||||||
# Recommends: matugen
|
|
||||||
# Recommends: wl-clipboard
|
|
||||||
# Recommends: NetworkManager
|
|
||||||
# Recommends: qt6-qtmultimedia
|
|
||||||
# Suggests: qt6ct
|
|
||||||
|
|
||||||
# %description
|
|
||||||
# DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
|
|
||||||
# and optimized for the niri and hyprland compositors. Features notifications,
|
|
||||||
# app launcher, wallpaper customization, and fully customizable with plugins.
|
|
||||||
|
|
||||||
# Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
|
|
||||||
# process monitoring, notification center, clipboard history, dock, control center,
|
|
||||||
# lock screen, and comprehensive plugin system.
|
|
||||||
|
|
||||||
# %package -n dms-cli
|
|
||||||
# Summary: DankMaterialShell CLI tool
|
|
||||||
# License: MIT
|
|
||||||
# URL: https://github.com/AvengeMedia/DankMaterialShell
|
|
||||||
|
|
||||||
# %description -n dms-cli
|
|
||||||
# Command-line interface for DankMaterialShell configuration and management.
|
|
||||||
# Provides native DBus bindings, NetworkManager integration, and system utilities.
|
|
||||||
|
|
||||||
# %prep
|
|
||||||
# %setup -q -c -n dms-qml
|
|
||||||
|
|
||||||
# # Download architecture-specific binaries during build
|
|
||||||
# case "%{_arch}" in
|
|
||||||
# x86_64)
|
|
||||||
# ARCH_SUFFIX="amd64"
|
|
||||||
# ;;
|
|
||||||
# aarch64)
|
|
||||||
# ARCH_SUFFIX="arm64"
|
|
||||||
# ;;
|
|
||||||
# *)
|
|
||||||
# echo "Unsupported architecture: %{_arch}"
|
|
||||||
# exit 1
|
|
||||||
# ;;
|
|
||||||
# esac
|
|
||||||
|
|
||||||
# wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
|
|
||||||
# echo "Failed to download dms-cli for architecture %{_arch}"
|
|
||||||
# exit 1
|
|
||||||
# }
|
|
||||||
# gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
|
|
||||||
# chmod +x %{_builddir}/dms-cli
|
|
||||||
|
|
||||||
# %build
|
|
||||||
|
|
||||||
# %install
|
|
||||||
# install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
|
||||||
|
|
||||||
# install -d %{buildroot}%{_datadir}/bash-completion/completions
|
|
||||||
# install -d %{buildroot}%{_datadir}/zsh/site-functions
|
|
||||||
# install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
|
|
||||||
# %{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
|
|
||||||
# %{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
|
|
||||||
# %{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
|
|
||||||
|
|
||||||
# install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
|
||||||
|
|
||||||
# install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
|
|
||||||
# install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
|
||||||
|
|
||||||
# install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
|
||||||
# cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
|
|
||||||
|
|
||||||
# rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
|
|
||||||
# rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
|
|
||||||
# rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
|
||||||
# rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
|
||||||
|
|
||||||
# echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
|
|
||||||
|
|
||||||
# %posttrans
|
|
||||||
# if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
|
|
||||||
# rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
|
|
||||||
# rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
|
|
||||||
# rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
|
|
||||||
# fi
|
|
||||||
# # Signal running DMS instances to reload
|
|
||||||
# pkill -USR1 -x dms >/dev/null 2>&1 || :
|
|
||||||
|
|
||||||
# %files
|
|
||||||
# %license LICENSE
|
|
||||||
# %doc README.md CONTRIBUTING.md
|
|
||||||
# %{_datadir}/quickshell/dms/
|
|
||||||
# %{_userunitdir}/dms.service
|
|
||||||
# %{_datadir}/applications/dms-open.desktop
|
|
||||||
# %{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
|
||||||
|
|
||||||
# %files -n dms-cli
|
|
||||||
# %{_bindir}/dms
|
|
||||||
# %{_datadir}/bash-completion/completions/dms
|
|
||||||
# %{_datadir}/zsh/site-functions/_dms
|
|
||||||
# %{_datadir}/fish/vendor_completions.d/dms.fish
|
|
||||||
|
|
||||||
# %changelog
|
|
||||||
# * CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1
|
|
||||||
# - Stable release VERSION_PLACEHOLDER
|
|
||||||
# - Built from GitHub release
|
|
||||||
# SPECEOF
|
|
||||||
|
|
||||||
# sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
|
|
||||||
# sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
|
|
||||||
|
|
||||||
# - name: Build SRPM
|
|
||||||
# id: build
|
|
||||||
# run: |
|
|
||||||
# cd ~/rpmbuild/SPECS
|
|
||||||
# rpmbuild -bs dms.spec
|
|
||||||
|
|
||||||
# SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1)
|
|
||||||
# SRPM_NAME=$(basename "$SRPM")
|
|
||||||
|
|
||||||
# echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
|
|
||||||
# echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
|
|
||||||
# echo "SRPM built: $SRPM_NAME"
|
|
||||||
|
|
||||||
# - name: Upload SRPM artifact
|
|
||||||
# uses: actions/upload-artifact@v4
|
|
||||||
# with:
|
|
||||||
# name: dms-stable-srpm-${{ steps.version.outputs.version }}
|
|
||||||
# path: ${{ steps.build.outputs.srpm_path }}
|
|
||||||
# retention-days: 90
|
|
||||||
|
|
||||||
# - name: Install Copr CLI
|
|
||||||
# run: |
|
|
||||||
# sudo apt-get install -y python3-pip
|
|
||||||
# pip3 install copr-cli
|
|
||||||
|
|
||||||
# mkdir -p ~/.config
|
|
||||||
# cat > ~/.config/copr << EOF
|
|
||||||
# [copr-cli]
|
|
||||||
# login = ${{ secrets.COPR_LOGIN }}
|
|
||||||
# username = avengemedia
|
|
||||||
# token = ${{ secrets.COPR_TOKEN }}
|
|
||||||
# copr_url = https://copr.fedorainfracloud.org
|
|
||||||
# EOF
|
|
||||||
# chmod 600 ~/.config/copr
|
|
||||||
|
|
||||||
# - name: Upload to Copr
|
|
||||||
# run: |
|
|
||||||
# SRPM="${{ steps.build.outputs.srpm_path }}"
|
|
||||||
# VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
|
|
||||||
# echo "Uploading SRPM to avengemedia/dms..."
|
|
||||||
# BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
|
|
||||||
# echo "$BUILD_OUTPUT"
|
|
||||||
|
|
||||||
# BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
|
|
||||||
|
|
||||||
# if [ "$BUILD_ID" != "unknown" ]; then
|
|
||||||
# echo "Build submitted: https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
|
|
||||||
# fi
|
|
||||||
|
|||||||
218
.github/workflows/run-copr.yml
vendored
218
.github/workflows/run-copr.yml
vendored
@@ -3,8 +3,17 @@ name: DMS Copr Stable Release
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
|
package:
|
||||||
|
description: 'Package to build (dms, dms-greeter, or both)'
|
||||||
|
required: false
|
||||||
|
default: 'dms'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- dms
|
||||||
|
- dms-greeter
|
||||||
|
- both
|
||||||
version:
|
version:
|
||||||
description: 'Versioning (e.g., 0.1.14, leave empty for latest release)'
|
description: 'Versioning (e.g., 1.0.3, leave empty for latest release)'
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
release:
|
release:
|
||||||
@@ -13,8 +22,27 @@ on:
|
|||||||
default: '1'
|
default: '1'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-upload:
|
determine-packages:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
packages: ${{ steps.set-packages.outputs.packages }}
|
||||||
|
steps:
|
||||||
|
- name: Set package list
|
||||||
|
id: set-packages
|
||||||
|
run: |
|
||||||
|
PACKAGE_INPUT="${{ github.event.inputs.package || 'dms' }}"
|
||||||
|
if [ "$PACKAGE_INPUT" = "both" ]; then
|
||||||
|
echo 'packages=["dms","dms-greeter"]' >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
build-and-upload:
|
||||||
|
needs: determine-packages
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
package: ${{ fromJSON(needs.determine-packages.outputs.packages) }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -39,7 +67,7 @@ jobs:
|
|||||||
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "release=$RELEASE" >> $GITHUB_OUTPUT
|
echo "release=$RELEASE" >> $GITHUB_OUTPUT
|
||||||
echo "✅ Building DMS hotfix version: $VERSION-$RELEASE"
|
echo "✅ Building ${{ matrix.package }} version: $VERSION-$RELEASE"
|
||||||
|
|
||||||
- name: Setup build environment
|
- name: Setup build environment
|
||||||
run: |
|
run: |
|
||||||
@@ -70,157 +98,31 @@ jobs:
|
|||||||
VERSION="${{ steps.version.outputs.version }}"
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
RELEASE="${{ steps.version.outputs.release }}"
|
RELEASE="${{ steps.version.outputs.release }}"
|
||||||
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
||||||
|
PACKAGE="${{ matrix.package }}"
|
||||||
|
|
||||||
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
|
# Copy spec file from repository
|
||||||
# Spec for DMS stable releases - Generated by GitHub Actions
|
cp distro/fedora/${PACKAGE}.spec ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||||
|
|
||||||
%global debug_package %{nil}
|
# Replace placeholders with actual values
|
||||||
%global version VERSION_PLACEHOLDER
|
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||||
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
sed -i "s/RELEASE_PLACEHOLDER/${RELEASE}/g" ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||||
|
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||||
|
|
||||||
Name: dms
|
echo "✅ Spec file generated for ${PACKAGE} v${VERSION}-${RELEASE}"
|
||||||
Version: %{version}
|
|
||||||
Release: RELEASE_PLACEHOLDER%{?dist}
|
|
||||||
Summary: %{pkg_summary}
|
|
||||||
|
|
||||||
License: MIT
|
|
||||||
URL: https://github.com/AvengeMedia/DankMaterialShell
|
|
||||||
|
|
||||||
Source0: dms-qml.tar.gz
|
|
||||||
|
|
||||||
BuildRequires: gzip
|
|
||||||
BuildRequires: wget
|
|
||||||
BuildRequires: systemd-rpm-macros
|
|
||||||
|
|
||||||
Requires: (quickshell or quickshell-git)
|
|
||||||
Requires: accountsservice
|
|
||||||
Requires: dms-cli = %{version}-%{release}
|
|
||||||
Requires: dgop
|
|
||||||
|
|
||||||
Recommends: cava
|
|
||||||
Recommends: cliphist
|
|
||||||
Recommends: danksearch
|
|
||||||
Recommends: hyprpicker
|
|
||||||
Recommends: matugen
|
|
||||||
Recommends: wl-clipboard
|
|
||||||
Recommends: NetworkManager
|
|
||||||
Recommends: qt6-qtmultimedia
|
|
||||||
Suggests: qt6ct
|
|
||||||
|
|
||||||
%description
|
|
||||||
DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
|
|
||||||
and optimized for the niri and hyprland compositors. Features notifications,
|
|
||||||
app launcher, wallpaper customization, and fully customizable with plugins.
|
|
||||||
|
|
||||||
Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
|
|
||||||
process monitoring, notification center, clipboard history, dock, control center,
|
|
||||||
lock screen, and comprehensive plugin system.
|
|
||||||
|
|
||||||
%package -n dms-cli
|
|
||||||
Summary: DankMaterialShell CLI tool
|
|
||||||
License: MIT
|
|
||||||
URL: https://github.com/AvengeMedia/DankMaterialShell
|
|
||||||
|
|
||||||
%description -n dms-cli
|
|
||||||
Command-line interface for DankMaterialShell configuration and management.
|
|
||||||
Provides native DBus bindings, NetworkManager integration, and system utilities.
|
|
||||||
|
|
||||||
%prep
|
|
||||||
%setup -q -c -n dms-qml
|
|
||||||
|
|
||||||
# Download architecture-specific binaries during build
|
|
||||||
# This ensures the correct architecture is used for each build target
|
|
||||||
case "%{_arch}" in
|
|
||||||
x86_64)
|
|
||||||
ARCH_SUFFIX="amd64"
|
|
||||||
;;
|
|
||||||
aarch64)
|
|
||||||
ARCH_SUFFIX="arm64"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unsupported architecture: %{_arch}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Download dms-cli for target architecture
|
|
||||||
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
|
|
||||||
echo "Failed to download dms-cli for architecture %{_arch}"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
|
|
||||||
chmod +x %{_builddir}/dms-cli
|
|
||||||
|
|
||||||
%build
|
|
||||||
|
|
||||||
%install
|
|
||||||
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
|
||||||
|
|
||||||
# Shell completions
|
|
||||||
install -d %{buildroot}%{_datadir}/bash-completion/completions
|
|
||||||
install -d %{buildroot}%{_datadir}/zsh/site-functions
|
|
||||||
install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
|
|
||||||
%{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
|
|
||||||
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
|
|
||||||
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
|
|
||||||
|
|
||||||
install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
|
||||||
|
|
||||||
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
|
||||||
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
|
|
||||||
|
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
|
|
||||||
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
|
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
|
||||||
|
|
||||||
%posttrans
|
|
||||||
# Clean up old installation path from previous versions (only if empty)
|
|
||||||
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
|
|
||||||
# Remove directories only if empty (preserves any user-added files)
|
|
||||||
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
|
|
||||||
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
|
|
||||||
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
# Signal running DMS instances to reload (harmless if none running)
|
|
||||||
pkill -USR1 -x dms >/dev/null 2>&1 || :
|
|
||||||
|
|
||||||
%files
|
|
||||||
%license LICENSE
|
|
||||||
%doc README.md CONTRIBUTING.md
|
|
||||||
%{_datadir}/quickshell/dms/
|
|
||||||
%{_userunitdir}/dms.service
|
|
||||||
|
|
||||||
%files -n dms-cli
|
|
||||||
%{_bindir}/dms
|
|
||||||
%{_datadir}/bash-completion/completions/dms
|
|
||||||
%{_datadir}/zsh/site-functions/_dms
|
|
||||||
%{_datadir}/fish/vendor_completions.d/dms.fish
|
|
||||||
|
|
||||||
%changelog
|
|
||||||
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER
|
|
||||||
- Stable release VERSION_PLACEHOLDER
|
|
||||||
- Built from GitHub release
|
|
||||||
SPECEOF
|
|
||||||
|
|
||||||
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
|
|
||||||
sed -i "s/RELEASE_PLACEHOLDER/${RELEASE}/g" ~/rpmbuild/SPECS/dms.spec
|
|
||||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
|
|
||||||
|
|
||||||
echo "✅ Spec file generated for v${VERSION}-${RELEASE}"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Spec file preview ==="
|
echo "=== Spec file preview ==="
|
||||||
head -40 ~/rpmbuild/SPECS/dms.spec
|
head -40 ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||||
|
|
||||||
- name: Build SRPM
|
- name: Build SRPM
|
||||||
id: build
|
id: build
|
||||||
run: |
|
run: |
|
||||||
cd ~/rpmbuild/SPECS
|
cd ~/rpmbuild/SPECS
|
||||||
|
PACKAGE="${{ matrix.package }}"
|
||||||
|
|
||||||
echo "🔨 Building SRPM..."
|
echo "🔨 Building SRPM for ${PACKAGE}..."
|
||||||
rpmbuild -bs dms.spec
|
rpmbuild -bs ${PACKAGE}.spec
|
||||||
|
|
||||||
SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1)
|
SRPM=$(ls ~/rpmbuild/SRPMS/${PACKAGE}-*.src.rpm | tail -n 1)
|
||||||
SRPM_NAME=$(basename "$SRPM")
|
SRPM_NAME=$(basename "$SRPM")
|
||||||
|
|
||||||
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
|
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
|
||||||
@@ -234,7 +136,7 @@ jobs:
|
|||||||
- name: Upload SRPM artifact
|
- name: Upload SRPM artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dms-stable-srpm-${{ steps.version.outputs.version }}
|
name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }}
|
||||||
path: ${{ steps.build.outputs.srpm_path }}
|
path: ${{ steps.build.outputs.srpm_path }}
|
||||||
retention-days: 90
|
retention-days: 90
|
||||||
|
|
||||||
@@ -255,23 +157,40 @@ jobs:
|
|||||||
|
|
||||||
echo "✅ Copr CLI configured"
|
echo "✅ Copr CLI configured"
|
||||||
|
|
||||||
|
- name: Determine Copr project
|
||||||
|
id: copr_project
|
||||||
|
run: |
|
||||||
|
PACKAGE="${{ matrix.package }}"
|
||||||
|
if [ "$PACKAGE" = "dms" ]; then
|
||||||
|
COPR_PROJECT="avengemedia/dms"
|
||||||
|
elif [ "$PACKAGE" = "dms-greeter" ]; then
|
||||||
|
COPR_PROJECT="avengemedia/danklinux"
|
||||||
|
else
|
||||||
|
echo "❌ Unknown package: $PACKAGE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "copr_project=$COPR_PROJECT" >> $GITHUB_OUTPUT
|
||||||
|
echo "✅ Copr project: $COPR_PROJECT"
|
||||||
|
|
||||||
- name: Upload to Copr
|
- name: Upload to Copr
|
||||||
run: |
|
run: |
|
||||||
SRPM="${{ steps.build.outputs.srpm_path }}"
|
SRPM="${{ steps.build.outputs.srpm_path }}"
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
COPR_PROJECT="${{ steps.copr_project.outputs.copr_project }}"
|
||||||
|
PACKAGE="${{ matrix.package }}"
|
||||||
|
|
||||||
echo "🚀 Uploading SRPM to avengemedia/dms..."
|
echo "🚀 Uploading ${PACKAGE} SRPM to ${COPR_PROJECT}..."
|
||||||
echo " SRPM: $(basename $SRPM)"
|
echo " SRPM: $(basename $SRPM)"
|
||||||
echo " Version: $VERSION"
|
echo " Version: $VERSION"
|
||||||
|
|
||||||
BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
|
BUILD_OUTPUT=$(copr-cli build "$COPR_PROJECT" "$SRPM" --nowait 2>&1)
|
||||||
echo "$BUILD_OUTPUT"
|
echo "$BUILD_OUTPUT"
|
||||||
|
|
||||||
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
|
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
|
||||||
|
|
||||||
if [ "$BUILD_ID" != "unknown" ]; then
|
if [ "$BUILD_ID" != "unknown" ]; then
|
||||||
echo "✅ Build submitted successfully!"
|
echo "✅ Build submitted successfully!"
|
||||||
echo "🔗 https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
|
echo "🔗 https://copr.fedorainfracloud.org/coprs/${COPR_PROJECT}/build/$BUILD_ID/"
|
||||||
else
|
else
|
||||||
echo "⚠️ Could not extract build ID, but upload may have succeeded"
|
echo "⚠️ Could not extract build ID, but upload may have succeeded"
|
||||||
fi
|
fi
|
||||||
@@ -279,10 +198,13 @@ jobs:
|
|||||||
- name: Build summary
|
- name: Build summary
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
echo "### 🎉 DMS Stable Build Summary" >> $GITHUB_STEP_SUMMARY
|
PACKAGE="${{ matrix.package }}"
|
||||||
|
COPR_PROJECT="${{ steps.copr_project.outputs.copr_project }}"
|
||||||
|
echo "### 🎉 ${PACKAGE} Stable Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **Package:** ${PACKAGE}" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **Version:** ${{ steps.version.outputs.version }}-${{ steps.version.outputs.release }}" >> $GITHUB_STEP_SUMMARY
|
echo "- **Version:** ${{ steps.version.outputs.version }}-${{ steps.version.outputs.release }}" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **SRPM:** ${{ steps.build.outputs.srpm_name }}" >> $GITHUB_STEP_SUMMARY
|
echo "- **SRPM:** ${{ steps.build.outputs.srpm_name }}" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **Project:** https://copr.fedorainfracloud.org/coprs/avengemedia/dms/" >> $GITHUB_STEP_SUMMARY
|
echo "- **Project:** https://copr.fedorainfracloud.org/coprs/${COPR_PROJECT}/" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Stable release has been built and uploaded to Copr!" >> $GITHUB_STEP_SUMMARY
|
echo "Stable release has been built and uploaded to Copr!" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|||||||
71
.github/workflows/run-obs.yml
vendored
71
.github/workflows/run-obs.yml
vendored
@@ -7,13 +7,14 @@ on:
|
|||||||
description: "Package to update (dms, dms-git, or all)"
|
description: "Package to update (dms, dms-git, or all)"
|
||||||
required: false
|
required: false
|
||||||
default: "all"
|
default: "all"
|
||||||
|
tag_version:
|
||||||
|
description: "Specific tag version for dms stable (e.g., v1.0.2). Leave empty to auto-detect latest release."
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
rebuild_release:
|
rebuild_release:
|
||||||
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
|
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
|
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
|
||||||
|
|
||||||
@@ -97,7 +98,7 @@ jobs:
|
|||||||
# Rebuild requested - always proceed
|
# Rebuild requested - always proceed
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
echo "🔄 Manual rebuild requested: $PKG (ppa$REBUILD)"
|
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
|
||||||
|
|
||||||
elif [[ "$PKG" == "all" ]]; then
|
elif [[ "$PKG" == "all" ]]; then
|
||||||
# Check each package and build list of those needing updates
|
# Check each package and build list of those needing updates
|
||||||
@@ -161,16 +162,51 @@ jobs:
|
|||||||
id: packages
|
id: packages
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
|
# Tag push event - use the pushed tag
|
||||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "Triggered by tag: $VERSION"
|
echo "Triggered by tag: $VERSION"
|
||||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
|
# Scheduled run - dms-git only
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
echo "Triggered by schedule: updating git package"
|
echo "Triggered by schedule: updating git package"
|
||||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
# Use filtered packages from check-updates when package="all" and no rebuild requested
|
# Manual workflow dispatch
|
||||||
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]]; then
|
|
||||||
|
# Determine version for dms stable
|
||||||
|
if [[ "${{ github.event.inputs.package }}" == "dms" ]]; then
|
||||||
|
# For explicit dms selection, require tag_version
|
||||||
|
if [[ -n "${{ github.event.inputs.tag_version }}" ]]; then
|
||||||
|
VERSION="${{ github.event.inputs.tag_version }}"
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Using specified tag: $VERSION"
|
||||||
|
else
|
||||||
|
echo "ERROR: tag_version is required when package=dms"
|
||||||
|
echo "Please specify a tag version (e.g., v1.0.2) or use package=all for auto-detection"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
elif [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
||||||
|
# For "all", auto-detect if tag_version not specified
|
||||||
|
if [[ -n "${{ github.event.inputs.tag_version }}" ]]; then
|
||||||
|
VERSION="${{ github.event.inputs.tag_version }}"
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Using specified tag: $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
|
||||||
|
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]] && [[ -z "${{ github.event.inputs.tag_version }}" ]]; then
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
echo "Manual trigger: all (filtered to: ${{ needs.check-updates.outputs.packages }})"
|
echo "Manual trigger: all (filtered to: ${{ needs.check-updates.outputs.packages }})"
|
||||||
else
|
else
|
||||||
@@ -186,7 +222,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||||
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
|
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "1.0.2")
|
||||||
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
||||||
echo "📦 Updating dms-git.spec to version: $NEW_VERSION"
|
echo "📦 Updating dms-git.spec to version: $NEW_VERSION"
|
||||||
|
|
||||||
@@ -207,14 +243,14 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||||
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
|
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "1.0.2")
|
||||||
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
||||||
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
|
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
|
||||||
|
|
||||||
# Single changelog entry (git snapshots don't need history)
|
# Single changelog entry (git snapshots don't need history)
|
||||||
CHANGELOG_DATE=$(date -R)
|
CHANGELOG_DATE=$(date -R)
|
||||||
{
|
{
|
||||||
echo "dms-git ($NEW_VERSION) nightly; urgency=medium"
|
echo "dms-git (${NEW_VERSION}db1) nightly; urgency=medium"
|
||||||
echo ""
|
echo ""
|
||||||
echo " * Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
|
echo " * Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -226,10 +262,15 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.packages.outputs.version }}"
|
VERSION="${{ steps.packages.outputs.version }}"
|
||||||
VERSION_NO_V="${VERSION#v}"
|
VERSION_NO_V="${VERSION#v}"
|
||||||
echo "Updating packaging to version $VERSION_NO_V"
|
echo "==> Updating packaging files to version: $VERSION_NO_V"
|
||||||
|
|
||||||
|
# Update spec file
|
||||||
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)
|
||||||
|
echo "✓ Spec file now shows Version: $UPDATED_VERSION"
|
||||||
|
|
||||||
# Single changelog entry (full history on OBS website)
|
# 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)
|
||||||
@@ -256,13 +297,13 @@ jobs:
|
|||||||
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
|
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
|
||||||
CHANGELOG_DATE=$(date -R)
|
CHANGELOG_DATE=$(date -R)
|
||||||
{
|
{
|
||||||
echo "dms ($VERSION_NO_V) stable; urgency=medium"
|
echo "dms (${VERSION_NO_V}db1) stable; urgency=medium"
|
||||||
echo ""
|
echo ""
|
||||||
echo " * Update to $VERSION stable release"
|
echo " * Update to $VERSION stable release"
|
||||||
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"
|
echo "✓ Updated Debian changelog to ${VERSION_NO_V}db1"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
@@ -289,6 +330,7 @@ jobs:
|
|||||||
- name: Upload to OBS
|
- name: Upload to OBS
|
||||||
env:
|
env:
|
||||||
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
||||||
|
TAG_VERSION: ${{ steps.packages.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
|
|
||||||
@@ -300,6 +342,7 @@ jobs:
|
|||||||
MESSAGE="Automated update from GitHub Actions"
|
MESSAGE="Automated update from GitHub Actions"
|
||||||
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||||
MESSAGE="Update to ${{ steps.packages.outputs.version }}"
|
MESSAGE="Update to ${{ steps.packages.outputs.version }}"
|
||||||
|
echo "==> Version being uploaded: ${{ steps.packages.outputs.version }}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 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)
|
||||||
@@ -309,7 +352,7 @@ jobs:
|
|||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
echo "Uploading $PKG to OBS..."
|
echo "Uploading $PKG to OBS..."
|
||||||
if [[ -n "$REBUILD_RELEASE" ]]; then
|
if [[ -n "$REBUILD_RELEASE" ]]; then
|
||||||
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
echo "🔄 Using rebuild release number: db$REBUILD_RELEASE"
|
||||||
fi
|
fi
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
@@ -350,7 +393,7 @@ jobs:
|
|||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
|
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
|
||||||
echo "**Rebuild Number:** ppa${{ github.event.inputs.rebuild_release }}" >> $GITHUB_STEP_SUMMARY
|
echo "**Rebuild Number:** db${{ github.event.inputs.rebuild_release }}" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
3
.github/workflows/run-ppa.yml
vendored
3
.github/workflows/run-ppa.yml
vendored
@@ -51,7 +51,8 @@ jobs:
|
|||||||
check_stable_package() {
|
check_stable_package() {
|
||||||
local PKG="$1"
|
local PKG="$1"
|
||||||
local PPA_NAME="$2"
|
local PPA_NAME="$2"
|
||||||
local LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "v\?\([^"]*\)".*/\1/' || echo "")
|
# Use git ls-remote to find the latest tag, sorted by version (descending)
|
||||||
|
local LATEST_TAG=$(git ls-remote --tags --refs --sort='-v:refname' https://github.com/AvengeMedia/DankMaterialShell.git | head -n1 | awk -F/ '{print $NF}' | sed 's/^v//')
|
||||||
local PPA_VERSION=$(curl -s "https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$PKG&status=Published" | grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
|
local PPA_VERSION=$(curl -s "https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$PKG&status=Published" | grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
|
||||||
local PPA_BASE_VERSION=$(echo "$PPA_VERSION" | sed 's/ppa[0-9]*$//')
|
local PPA_BASE_VERSION=$(echo "$PPA_VERSION" | sed 's/ppa[0-9]*$//')
|
||||||
|
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -96,11 +96,12 @@ go.work
|
|||||||
go.work.sum
|
go.work.sum
|
||||||
|
|
||||||
# env file
|
# env file
|
||||||
.env
|
.env*
|
||||||
|
|
||||||
# Editor/IDE
|
# Editor/IDE
|
||||||
# .idea/
|
# .idea/
|
||||||
# .vscode/
|
# .vscode/
|
||||||
|
vim/
|
||||||
|
|
||||||
bin/
|
bin/
|
||||||
|
|
||||||
|
|||||||
14
CHANGELOG.MD
14
CHANGELOG.MD
@@ -1,6 +1,20 @@
|
|||||||
|
This file is more of a quick reference so I know what to account for before next releases.
|
||||||
|
|
||||||
# 1.2.0
|
# 1.2.0
|
||||||
|
|
||||||
- Added clipboard and clipboard history integration
|
- Added clipboard and clipboard history integration
|
||||||
- Added swipe to dismiss notification popups and from center
|
- Added swipe to dismiss notification popups and from center
|
||||||
- Added paste from clipboard history view - requires wtype
|
- Added paste from clipboard history view - requires wtype
|
||||||
- Optimize surface damage of OSD & Toast
|
- Optimize surface damage of OSD & Toast
|
||||||
|
- Add monitor configurator (niri, Hyprland, MangoWC)
|
||||||
|
- **BREAKING** ghostty theme changed to ~/.config/ghostty/themes/danktheme
|
||||||
|
- requires intervention and doc update
|
||||||
|
- Added desktop widget plugins
|
||||||
|
- dev guidance available
|
||||||
|
- builtin clock & dgop widgets
|
||||||
|
- new IPC targets
|
||||||
|
- Initial RTL support/i18n
|
||||||
|
- Theme registry
|
||||||
|
- Notification persistence & history
|
||||||
|
- **BREAKING** vscode theme needs re-installed
|
||||||
|
- dms doctor cmd
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ quickshell -p quickshell/
|
|||||||
inputs.dms.url = "github:AvengeMedia/DankMaterialShell";
|
inputs.dms.url = "github:AvengeMedia/DankMaterialShell";
|
||||||
|
|
||||||
# Use in home-manager or NixOS configuration
|
# Use in home-manager or NixOS configuration
|
||||||
imports = [ inputs.dms.homeModules.dankMaterialShell.default ];
|
imports = [ inputs.dms.homeModules.dank-material-shell ];
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=DMS Application Picker
|
Name=DMS
|
||||||
Comment=Select an application to open links and files
|
Comment=Select an application to open links and files
|
||||||
Exec=dms open %u
|
Exec=dms open %u
|
||||||
Icon=danklogo
|
Icon=danklogo
|
||||||
Terminal=false
|
Terminal=false
|
||||||
NoDisplay=true
|
NoDisplay=true
|
||||||
MimeType=x-scheme-handler/http;x-scheme-handler/https;text/html;application/xhtml+xml;
|
MimeType=x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/dms;text/html;application/xhtml+xml;
|
||||||
Categories=Utility;
|
Categories=Utility;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Type=dbus
|
|||||||
BusName=org.freedesktop.Notifications
|
BusName=org.freedesktop.Notifications
|
||||||
ExecStart=/usr/bin/dms run --session
|
ExecStart=/usr/bin/dms run --session
|
||||||
ExecReload=/usr/bin/pkill -USR1 -x dms
|
ExecReload=/usr/bin/pkill -USR1 -x dms
|
||||||
Restart=always
|
Restart=on-failure
|
||||||
RestartSec=1.23
|
RestartSec=1.23
|
||||||
TimeoutStopSec=10
|
TimeoutStopSec=10
|
||||||
|
|
||||||
|
|||||||
@@ -68,3 +68,9 @@ packages:
|
|||||||
outpkg: mocks_wlclient
|
outpkg: mocks_wlclient
|
||||||
interfaces:
|
interfaces:
|
||||||
WaylandDisplay:
|
WaylandDisplay:
|
||||||
|
github.com/AvengeMedia/DankMaterialShell/core/internal/utils:
|
||||||
|
config:
|
||||||
|
dir: "internal/mocks/utils"
|
||||||
|
outpkg: mocks_utils
|
||||||
|
interfaces:
|
||||||
|
AppChecker:
|
||||||
|
|||||||
@@ -14,34 +14,63 @@ Distribution-aware installer with TUI for deploying DMS and compositor configura
|
|||||||
|
|
||||||
## System Integration
|
## System Integration
|
||||||
|
|
||||||
**Wayland Protocols**
|
### Wayland Protocols (Client)
|
||||||
- `wlr-gamma-control-unstable-v1` - Night mode and gamma control
|
|
||||||
- `wlr-screencopy-unstable-v1` - Screen capture for color picker
|
|
||||||
- `wlr-layer-shell-unstable-v1` - Overlay surfaces for color picker
|
|
||||||
- `wp-viewporter` - Fractional scaling support
|
|
||||||
- `dwl-ipc-unstable-v2` - dwl/MangoWC workspace integration
|
|
||||||
- `ext-workspace-v1` - Workspace protocol support
|
|
||||||
- `wlr-output-management-unstable-v1` - Display configuration
|
|
||||||
|
|
||||||
**DBus Interfaces**
|
All Wayland protocols are consumed as a client - connecting to the compositor.
|
||||||
- NetworkManager/iwd - Network management
|
|
||||||
- logind - Session control and inhibit locks
|
|
||||||
- accountsservice - User account information
|
|
||||||
- CUPS - Printer management
|
|
||||||
- Custom IPC via unix socket (JSON API)
|
|
||||||
|
|
||||||
**Hardware Control**
|
| Protocol | Purpose |
|
||||||
- DDC/CI protocol - External monitor brightness control (like `ddcutil`)
|
| ----------------------------------------- | ----------------------------------------------------------- |
|
||||||
- Backlight control - Internal display brightness via `login1` or sysfs
|
| `wlr-gamma-control-unstable-v1` | Night mode color temperature control |
|
||||||
- LED control - Keyboard/device LED management
|
| `wlr-screencopy-unstable-v1` | Screen capture for color picker/screenshot |
|
||||||
- evdev input monitoring - Keyboard state tracking (caps lock, etc.)
|
| `wlr-layer-shell-unstable-v1` | Overlay surfaces for color picker UI/screenshot |
|
||||||
|
| `wlr-output-management-unstable-v1` | Display configuration |
|
||||||
|
| `wlr-output-power-management-unstable-v1` | DPMS on/off CLI |
|
||||||
|
| `wp-viewporter` | Fractional scaling support (color picker/screenshot UIs) |
|
||||||
|
| `keyboard-shortcuts-inhibit-unstable-v1` | Inhibit compositor shortcuts during color picker/screenshot |
|
||||||
|
| `ext-data-control-v1` | Clipboard history and persistence |
|
||||||
|
| `ext-workspace-v1` | Workspace integration |
|
||||||
|
| `dwl-ipc-unstable-v2` | dwl/MangoWC IPC for tags, outputs, etc. |
|
||||||
|
|
||||||
|
### DBus Interfaces
|
||||||
|
|
||||||
|
**Client (consuming external services):**
|
||||||
|
|
||||||
|
| Interface | Purpose |
|
||||||
|
| -------------------------------- | --------------------------------------------- |
|
||||||
|
| `org.bluez` | Bluetooth management with pairing agent |
|
||||||
|
| `org.freedesktop.NetworkManager` | Network management |
|
||||||
|
| `net.connman.iwd` | iwd Wi-Fi backend |
|
||||||
|
| `org.freedesktop.network1` | systemd-networkd integration |
|
||||||
|
| `org.freedesktop.login1` | Session control, sleep inhibitors, brightness |
|
||||||
|
| `org.freedesktop.Accounts` | User account information |
|
||||||
|
| `org.freedesktop.portal.Desktop` | Desktop appearance settings (color scheme) |
|
||||||
|
| CUPS via IPP + D-Bus | Printer management with job notifications |
|
||||||
|
|
||||||
|
**Server (implementing interfaces):**
|
||||||
|
|
||||||
|
| Interface | Purpose |
|
||||||
|
| ----------------------------- | -------------------------------------- |
|
||||||
|
| `org.freedesktop.ScreenSaver` | Screensaver inhibit for video playback |
|
||||||
|
|
||||||
|
Custom IPC via unix socket (JSON API) for shell communication.
|
||||||
|
|
||||||
|
### Hardware Control
|
||||||
|
|
||||||
|
| Subsystem | Method | Purpose |
|
||||||
|
| --------- | ------------------- | ---------------------------------- |
|
||||||
|
| DDC/CI | I2C direct | External monitor brightness |
|
||||||
|
| Backlight | logind or sysfs | Internal display brightness |
|
||||||
|
| evdev | `/dev/input/event*` | Keyboard state (caps lock LED) |
|
||||||
|
| udev | netlink monitor | Backlight device updates (for OSD) |
|
||||||
|
|
||||||
|
### Plugin System
|
||||||
|
|
||||||
**Plugin System**
|
|
||||||
- Plugin registry integration
|
- Plugin registry integration
|
||||||
- Plugin lifecycle management
|
- Plugin lifecycle management
|
||||||
- Settings persistence
|
- Settings persistence
|
||||||
|
|
||||||
## CLI Commands
|
## CLI Commands
|
||||||
|
|
||||||
- `dms run [-d]` - Start shell (optionally as daemon)
|
- `dms run [-d]` - Start shell (optionally as daemon)
|
||||||
- `dms restart` / `dms kill` - Manage running processes
|
- `dms restart` / `dms kill` - Manage running processes
|
||||||
- `dms ipc <command>` - Send IPC commands (toggle launcher, notifications, etc.)
|
- `dms ipc <command>` - Send IPC commands (toggle launcher, notifications, etc.)
|
||||||
@@ -70,6 +99,7 @@ The on-screen preview displays the selected format. JSON output includes hex, RG
|
|||||||
Requires Go 1.24+
|
Requires Go 1.24+
|
||||||
|
|
||||||
**Development build:**
|
**Development build:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make # Build dms CLI
|
make # Build dms CLI
|
||||||
make dankinstall # Build installer
|
make dankinstall # Build installer
|
||||||
@@ -77,6 +107,7 @@ make test # Run tests
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Distribution build:**
|
**Distribution build:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make dist # Build without update/greeter features
|
make dist # Build without update/greeter features
|
||||||
```
|
```
|
||||||
@@ -84,6 +115,7 @@ make dist # Build without update/greeter features
|
|||||||
Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64`
|
Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64`
|
||||||
|
|
||||||
**Installation:**
|
**Installation:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo make install # Install to /usr/local/bin/dms
|
sudo make install # Install to /usr/local/bin/dms
|
||||||
```
|
```
|
||||||
@@ -91,6 +123,7 @@ sudo make install # Install to /usr/local/bin/dms
|
|||||||
## Development
|
## Development
|
||||||
|
|
||||||
**Setup pre-commit hooks:**
|
**Setup pre-commit hooks:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git config core.hooksPath .githooks
|
git config core.hooksPath .githooks
|
||||||
```
|
```
|
||||||
@@ -98,6 +131,7 @@ git config core.hooksPath .githooks
|
|||||||
This runs gofmt, golangci-lint, tests, and builds before each commit when `core/` files are staged.
|
This runs gofmt, golangci-lint, tests, and builds before each commit when `core/` files are staged.
|
||||||
|
|
||||||
**Regenerating Wayland Protocol Bindings:**
|
**Regenerating Wayland Protocol Bindings:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go install github.com/rajveermalviya/go-wayland/cmd/go-wayland-scanner@latest
|
go install github.com/rajveermalviya/go-wayland/cmd/go-wayland-scanner@latest
|
||||||
go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
|
go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
|
||||||
@@ -105,6 +139,7 @@ go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Module Structure:**
|
**Module Structure:**
|
||||||
|
|
||||||
- `cmd/` - Binary entrypoints (dms, dankinstall)
|
- `cmd/` - Binary entrypoints (dms, dankinstall)
|
||||||
- `internal/distros/` - Distribution-specific installation logic
|
- `internal/distros/` - Distribution-specific installation logic
|
||||||
- `internal/proto/` - Wayland protocol bindings
|
- `internal/proto/` - Wayland protocol bindings
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ func runBrightnessList(cmd *cobra.Command, args []string) {
|
|||||||
fmt.Printf("%-*s %-12s %-*s %s\n", idPad, "Device", "Class", namePad, "Name", "Brightness")
|
fmt.Printf("%-*s %-12s %-*s %s\n", idPad, "Device", "Class", namePad, "Name", "Brightness")
|
||||||
|
|
||||||
sepLen := idPad + 2 + 12 + 2 + namePad + 2 + 15
|
sepLen := idPad + 2 + 12 + 2 + namePad + 2 + 15
|
||||||
for i := 0; i < sepLen; i++ {
|
for range sepLen {
|
||||||
fmt.Print("─")
|
fmt.Print("─")
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|||||||
@@ -142,10 +142,6 @@ var (
|
|||||||
clipConfigNoClearStartup bool
|
clipConfigNoClearStartup bool
|
||||||
clipConfigDisabled bool
|
clipConfigDisabled bool
|
||||||
clipConfigEnabled bool
|
clipConfigEnabled bool
|
||||||
clipConfigDisableHistory bool
|
|
||||||
clipConfigEnableHistory bool
|
|
||||||
clipConfigDisablePersist bool
|
|
||||||
clipConfigEnablePersist bool
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -169,12 +165,8 @@ func init() {
|
|||||||
clipConfigSetCmd.Flags().IntVar(&clipConfigAutoClearDays, "auto-clear-days", -1, "Auto-clear entries older than N days (0 to disable)")
|
clipConfigSetCmd.Flags().IntVar(&clipConfigAutoClearDays, "auto-clear-days", -1, "Auto-clear entries older than N days (0 to disable)")
|
||||||
clipConfigSetCmd.Flags().BoolVar(&clipConfigClearAtStartup, "clear-at-startup", false, "Clear history on startup")
|
clipConfigSetCmd.Flags().BoolVar(&clipConfigClearAtStartup, "clear-at-startup", false, "Clear history on startup")
|
||||||
clipConfigSetCmd.Flags().BoolVar(&clipConfigNoClearStartup, "no-clear-at-startup", false, "Don't clear history on startup")
|
clipConfigSetCmd.Flags().BoolVar(&clipConfigNoClearStartup, "no-clear-at-startup", false, "Don't clear history on startup")
|
||||||
clipConfigSetCmd.Flags().BoolVar(&clipConfigDisabled, "disable", false, "Disable clipboard manager entirely")
|
clipConfigSetCmd.Flags().BoolVar(&clipConfigDisabled, "disable", false, "Disable clipboard tracking")
|
||||||
clipConfigSetCmd.Flags().BoolVar(&clipConfigEnabled, "enable", false, "Enable clipboard manager")
|
clipConfigSetCmd.Flags().BoolVar(&clipConfigEnabled, "enable", false, "Enable clipboard tracking")
|
||||||
clipConfigSetCmd.Flags().BoolVar(&clipConfigDisableHistory, "disable-history", false, "Disable clipboard history persistence")
|
|
||||||
clipConfigSetCmd.Flags().BoolVar(&clipConfigEnableHistory, "enable-history", false, "Enable clipboard history persistence")
|
|
||||||
clipConfigSetCmd.Flags().BoolVar(&clipConfigDisablePersist, "disable-persist", false, "Disable clipboard ownership persistence")
|
|
||||||
clipConfigSetCmd.Flags().BoolVar(&clipConfigEnablePersist, "enable-persist", false, "Enable clipboard ownership persistence")
|
|
||||||
|
|
||||||
clipWatchCmd.Flags().BoolVarP(&clipWatchStore, "store", "s", false, "Store clipboard changes to history (no server required)")
|
clipWatchCmd.Flags().BoolVarP(&clipWatchStore, "store", "s", false, "Store clipboard changes to history (no server required)")
|
||||||
|
|
||||||
@@ -591,18 +583,6 @@ func runClipConfigSet(cmd *cobra.Command, args []string) {
|
|||||||
if clipConfigEnabled {
|
if clipConfigEnabled {
|
||||||
params["disabled"] = false
|
params["disabled"] = false
|
||||||
}
|
}
|
||||||
if clipConfigDisableHistory {
|
|
||||||
params["disableHistory"] = true
|
|
||||||
}
|
|
||||||
if clipConfigEnableHistory {
|
|
||||||
params["disableHistory"] = false
|
|
||||||
}
|
|
||||||
if clipConfigDisablePersist {
|
|
||||||
params["disablePersist"] = true
|
|
||||||
}
|
|
||||||
if clipConfigEnablePersist {
|
|
||||||
params["disablePersist"] = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(params) == 0 {
|
if len(params) == 0 {
|
||||||
fmt.Println("No config options specified")
|
fmt.Println("No config options specified")
|
||||||
|
|||||||
@@ -171,7 +171,6 @@ var pluginsUpdateCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runVersion(cmd *cobra.Command, args []string) {
|
func runVersion(cmd *cobra.Command, args []string) {
|
||||||
printASCII()
|
|
||||||
fmt.Printf("%s\n", formatVersion(Version))
|
fmt.Printf("%s\n", formatVersion(Version))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,7 +219,7 @@ func getBaseVersion() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback
|
// Fallback
|
||||||
return "0.6.2"
|
return "1.0.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
func startDebugServer() error {
|
func startDebugServer() error {
|
||||||
@@ -514,5 +513,6 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
notifyActionCmd,
|
notifyActionCmd,
|
||||||
matugenCmd,
|
matugenCmd,
|
||||||
clipboardCmd,
|
clipboardCmd,
|
||||||
|
doctorCmd,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ func init() {
|
|||||||
dank16Cmd.Flags().Bool("json", false, "Output in JSON format")
|
dank16Cmd.Flags().Bool("json", false, "Output in JSON format")
|
||||||
dank16Cmd.Flags().Bool("kitty", false, "Output in Kitty terminal format")
|
dank16Cmd.Flags().Bool("kitty", false, "Output in Kitty terminal format")
|
||||||
dank16Cmd.Flags().Bool("foot", false, "Output in Foot terminal format")
|
dank16Cmd.Flags().Bool("foot", false, "Output in Foot terminal format")
|
||||||
|
dank16Cmd.Flags().Bool("neovim", false, "Output in Neovim plugin format")
|
||||||
dank16Cmd.Flags().Bool("alacritty", false, "Output in Alacritty terminal format")
|
dank16Cmd.Flags().Bool("alacritty", false, "Output in Alacritty terminal format")
|
||||||
dank16Cmd.Flags().Bool("ghostty", false, "Output in Ghostty terminal format")
|
dank16Cmd.Flags().Bool("ghostty", false, "Output in Ghostty terminal format")
|
||||||
dank16Cmd.Flags().Bool("wezterm", false, "Output in Wezterm terminal format")
|
dank16Cmd.Flags().Bool("wezterm", false, "Output in Wezterm terminal format")
|
||||||
@@ -40,6 +41,7 @@ func runDank16(cmd *cobra.Command, args []string) {
|
|||||||
isJson, _ := cmd.Flags().GetBool("json")
|
isJson, _ := cmd.Flags().GetBool("json")
|
||||||
isKitty, _ := cmd.Flags().GetBool("kitty")
|
isKitty, _ := cmd.Flags().GetBool("kitty")
|
||||||
isFoot, _ := cmd.Flags().GetBool("foot")
|
isFoot, _ := cmd.Flags().GetBool("foot")
|
||||||
|
isNeovim, _ := cmd.Flags().GetBool("neovim")
|
||||||
isAlacritty, _ := cmd.Flags().GetBool("alacritty")
|
isAlacritty, _ := cmd.Flags().GetBool("alacritty")
|
||||||
isGhostty, _ := cmd.Flags().GetBool("ghostty")
|
isGhostty, _ := cmd.Flags().GetBool("ghostty")
|
||||||
isWezterm, _ := cmd.Flags().GetBool("wezterm")
|
isWezterm, _ := cmd.Flags().GetBool("wezterm")
|
||||||
@@ -116,6 +118,8 @@ func runDank16(cmd *cobra.Command, args []string) {
|
|||||||
fmt.Print(dank16.GenerateGhosttyTheme(colors))
|
fmt.Print(dank16.GenerateGhosttyTheme(colors))
|
||||||
} else if isWezterm {
|
} else if isWezterm {
|
||||||
fmt.Print(dank16.GenerateWeztermTheme(colors))
|
fmt.Print(dank16.GenerateWeztermTheme(colors))
|
||||||
|
} else if isNeovim {
|
||||||
|
fmt.Print(dank16.GenerateNeovimTheme(colors))
|
||||||
} else {
|
} else {
|
||||||
fmt.Print(dank16.GenerateGhosttyTheme(colors))
|
fmt.Print(dank16.GenerateGhosttyTheme(colors))
|
||||||
}
|
}
|
||||||
|
|||||||
930
core/cmd/dms/commands_doctor.go
Normal file
930
core/cmd/dms/commands_doctor.go
Normal file
@@ -0,0 +1,930 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type status string
|
||||||
|
|
||||||
|
const (
|
||||||
|
statusOK status = "ok"
|
||||||
|
statusWarn status = "warn"
|
||||||
|
statusError status = "error"
|
||||||
|
statusInfo status = "info"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s status) IconStyle(styles tui.Styles) (string, lipgloss.Style) {
|
||||||
|
switch s {
|
||||||
|
case statusOK:
|
||||||
|
return "●", styles.Success
|
||||||
|
case statusWarn:
|
||||||
|
return "●", styles.Warning
|
||||||
|
case statusError:
|
||||||
|
return "●", styles.Error
|
||||||
|
default:
|
||||||
|
return "○", styles.Subtle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DoctorStatus struct {
|
||||||
|
Errors []checkResult
|
||||||
|
Warnings []checkResult
|
||||||
|
OK []checkResult
|
||||||
|
Info []checkResult
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ds *DoctorStatus) Add(r checkResult) {
|
||||||
|
switch r.status {
|
||||||
|
case statusError:
|
||||||
|
ds.Errors = append(ds.Errors, r)
|
||||||
|
case statusWarn:
|
||||||
|
ds.Warnings = append(ds.Warnings, r)
|
||||||
|
case statusOK:
|
||||||
|
ds.OK = append(ds.OK, r)
|
||||||
|
case statusInfo:
|
||||||
|
ds.Info = append(ds.Info, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ds *DoctorStatus) HasIssues() bool {
|
||||||
|
return len(ds.Errors) > 0 || len(ds.Warnings) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ds *DoctorStatus) ErrorCount() int {
|
||||||
|
return len(ds.Errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ds *DoctorStatus) WarningCount() int {
|
||||||
|
return len(ds.Warnings)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ds *DoctorStatus) OKCount() int {
|
||||||
|
return len(ds.OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
quickshellVersionRegex = regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`)
|
||||||
|
hyprlandVersionRegex = regexp.MustCompile(`v?(\d+\.\d+\.\d+)`)
|
||||||
|
niriVersionRegex = regexp.MustCompile(`niri (\d+\.\d+)`)
|
||||||
|
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)
|
||||||
|
riverVersionRegex = regexp.MustCompile(`river (\d+\.\d+)`)
|
||||||
|
wayfireVersionRegex = regexp.MustCompile(`wayfire (\d+\.\d+)`)
|
||||||
|
)
|
||||||
|
|
||||||
|
var doctorCmd = &cobra.Command{
|
||||||
|
Use: "doctor",
|
||||||
|
Short: "Diagnose DMS installation and dependencies",
|
||||||
|
Long: "Check system health, verify dependencies, and diagnose configuration issues for DMS",
|
||||||
|
Run: runDoctor,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
doctorVerbose bool
|
||||||
|
doctorJSON bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
doctorCmd.Flags().BoolVarP(&doctorVerbose, "verbose", "v", false, "Show detailed output including paths and versions")
|
||||||
|
doctorCmd.Flags().BoolVarP(&doctorJSON, "json", "j", false, "Output results in JSON format")
|
||||||
|
}
|
||||||
|
|
||||||
|
type category int
|
||||||
|
|
||||||
|
const (
|
||||||
|
catSystem category = iota
|
||||||
|
catVersions
|
||||||
|
catInstallation
|
||||||
|
catCompositor
|
||||||
|
catQuickshellFeatures
|
||||||
|
catOptionalFeatures
|
||||||
|
catConfigFiles
|
||||||
|
catServices
|
||||||
|
catEnvironment
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c category) String() string {
|
||||||
|
switch c {
|
||||||
|
case catSystem:
|
||||||
|
return "System"
|
||||||
|
case catVersions:
|
||||||
|
return "Versions"
|
||||||
|
case catInstallation:
|
||||||
|
return "Installation"
|
||||||
|
case catCompositor:
|
||||||
|
return "Compositor"
|
||||||
|
case catQuickshellFeatures:
|
||||||
|
return "Quickshell Features"
|
||||||
|
case catOptionalFeatures:
|
||||||
|
return "Optional Features"
|
||||||
|
case catConfigFiles:
|
||||||
|
return "Config Files"
|
||||||
|
case catServices:
|
||||||
|
return "Services"
|
||||||
|
case catEnvironment:
|
||||||
|
return "Environment"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
checkNameMaxLength = 21
|
||||||
|
doctorDocsURL = "https://danklinux.com/docs/dankmaterialshell/cli-doctor"
|
||||||
|
)
|
||||||
|
|
||||||
|
type checkResult struct {
|
||||||
|
category category
|
||||||
|
name string
|
||||||
|
status status
|
||||||
|
message string
|
||||||
|
details string
|
||||||
|
url string
|
||||||
|
}
|
||||||
|
|
||||||
|
type checkResultJSON struct {
|
||||||
|
Category string `json:"category"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Details string `json:"details,omitempty"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type doctorOutputJSON struct {
|
||||||
|
Summary struct {
|
||||||
|
Errors int `json:"errors"`
|
||||||
|
Warnings int `json:"warnings"`
|
||||||
|
OK int `json:"ok"`
|
||||||
|
Info int `json:"info"`
|
||||||
|
} `json:"summary"`
|
||||||
|
Results []checkResultJSON `json:"results"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r checkResult) toJSON() checkResultJSON {
|
||||||
|
return checkResultJSON{
|
||||||
|
Category: r.category.String(),
|
||||||
|
Name: r.name,
|
||||||
|
Status: string(r.status),
|
||||||
|
Message: r.message,
|
||||||
|
Details: r.details,
|
||||||
|
URL: r.url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDoctor(cmd *cobra.Command, args []string) {
|
||||||
|
if !doctorJSON {
|
||||||
|
printDoctorHeader()
|
||||||
|
}
|
||||||
|
|
||||||
|
qsFeatures, qsMissingFeatures := checkQuickshellFeatures()
|
||||||
|
|
||||||
|
results := slices.Concat(
|
||||||
|
checkSystemInfo(),
|
||||||
|
checkVersions(qsMissingFeatures),
|
||||||
|
checkDMSInstallation(),
|
||||||
|
checkWindowManagers(),
|
||||||
|
qsFeatures,
|
||||||
|
checkOptionalDependencies(),
|
||||||
|
checkConfigurationFiles(),
|
||||||
|
checkSystemdServices(),
|
||||||
|
checkEnvironmentVars(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if doctorJSON {
|
||||||
|
printResultsJSON(results)
|
||||||
|
} else {
|
||||||
|
printResults(results)
|
||||||
|
printSummary(results, qsMissingFeatures)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printDoctorHeader() {
|
||||||
|
theme := tui.TerminalTheme()
|
||||||
|
styles := tui.NewStyles(theme)
|
||||||
|
|
||||||
|
fmt.Println(getThemedASCII())
|
||||||
|
fmt.Println(styles.Title.Render("System Health Check"))
|
||||||
|
fmt.Println(styles.Subtle.Render("──────────────────────────────────────"))
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkSystemInfo() []checkResult {
|
||||||
|
var results []checkResult
|
||||||
|
|
||||||
|
osInfo, err := distros.GetOSInfo()
|
||||||
|
if err != nil {
|
||||||
|
status, message, details := statusWarn, fmt.Sprintf("Unknown (%v)", err), ""
|
||||||
|
|
||||||
|
if strings.Contains(err.Error(), "Unsupported distribution") {
|
||||||
|
osRelease := readOSRelease()
|
||||||
|
switch {
|
||||||
|
case osRelease["ID"] == "nixos":
|
||||||
|
status = statusOK
|
||||||
|
message = osRelease["PRETTY_NAME"]
|
||||||
|
if message == "" {
|
||||||
|
message = fmt.Sprintf("NixOS %s", osRelease["VERSION_ID"])
|
||||||
|
}
|
||||||
|
details = "Supported for runtime (install via NixOS module or Flake)"
|
||||||
|
case osRelease["PRETTY_NAME"] != "":
|
||||||
|
message = fmt.Sprintf("%s (not supported by dms setup)", osRelease["PRETTY_NAME"])
|
||||||
|
details = "DMS may work but automatic installation is not available"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, checkResult{catSystem, "Operating System", status, message, details, doctorDocsURL + "#operating-system"})
|
||||||
|
} else {
|
||||||
|
status := statusOK
|
||||||
|
message := osInfo.PrettyName
|
||||||
|
if message == "" {
|
||||||
|
message = fmt.Sprintf("%s %s", osInfo.Distribution.ID, osInfo.VersionID)
|
||||||
|
}
|
||||||
|
if distros.IsUnsupportedDistro(osInfo.Distribution.ID, osInfo.VersionID) {
|
||||||
|
status = statusWarn
|
||||||
|
message += " (version may not be fully supported)"
|
||||||
|
}
|
||||||
|
results = append(results, checkResult{
|
||||||
|
catSystem, "Operating System", status, message,
|
||||||
|
fmt.Sprintf("ID: %s, Version: %s, Arch: %s", osInfo.Distribution.ID, osInfo.VersionID, osInfo.Architecture),
|
||||||
|
doctorDocsURL + "#operating-system",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
arch := runtime.GOARCH
|
||||||
|
archStatus := statusOK
|
||||||
|
if arch != "amd64" && arch != "arm64" {
|
||||||
|
archStatus = statusError
|
||||||
|
}
|
||||||
|
results = append(results, checkResult{catSystem, "Architecture", archStatus, arch, "", doctorDocsURL + "#architecture"})
|
||||||
|
|
||||||
|
waylandDisplay := os.Getenv("WAYLAND_DISPLAY")
|
||||||
|
xdgSessionType := os.Getenv("XDG_SESSION_TYPE")
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case waylandDisplay != "" || xdgSessionType == "wayland":
|
||||||
|
results = append(results, checkResult{
|
||||||
|
catSystem, "Display Server", statusOK, "Wayland",
|
||||||
|
fmt.Sprintf("WAYLAND_DISPLAY=%s", waylandDisplay),
|
||||||
|
doctorDocsURL + "#display-server",
|
||||||
|
})
|
||||||
|
case xdgSessionType == "x11":
|
||||||
|
results = append(results, checkResult{catSystem, "Display Server", statusError, "X11 (DMS requires Wayland)", "", doctorDocsURL + "#display-server"})
|
||||||
|
default:
|
||||||
|
results = append(results, checkResult{
|
||||||
|
catSystem, "Display Server", statusWarn, "Unknown (ensure you're running Wayland)",
|
||||||
|
fmt.Sprintf("XDG_SESSION_TYPE=%s", xdgSessionType),
|
||||||
|
doctorDocsURL + "#display-server",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkEnvironmentVars() []checkResult {
|
||||||
|
var results []checkResult
|
||||||
|
results = append(results, checkEnvVar("QT_QPA_PLATFORMTHEME")...)
|
||||||
|
results = append(results, checkEnvVar("QS_ICON_THEME")...)
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkEnvVar(name string) []checkResult {
|
||||||
|
value := os.Getenv(name)
|
||||||
|
if value != "" {
|
||||||
|
return []checkResult{{catEnvironment, name, statusInfo, value, "", doctorDocsURL + "#environment-variables"}}
|
||||||
|
}
|
||||||
|
if doctorVerbose {
|
||||||
|
return []checkResult{{catEnvironment, name, statusInfo, "Not set", "", doctorDocsURL + "#environment-variables"}}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readOSRelease() map[string]string {
|
||||||
|
result := make(map[string]string)
|
||||||
|
data, err := os.ReadFile("/etc/os-release")
|
||||||
|
if err != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
for line := range strings.SplitSeq(string(data), "\n") {
|
||||||
|
if parts := strings.SplitN(line, "=", 2); len(parts) == 2 {
|
||||||
|
result[parts[0]] = strings.Trim(parts[1], "\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkVersions(qsMissingFeatures bool) []checkResult {
|
||||||
|
dmsCliPath, _ := os.Executable()
|
||||||
|
dmsCliDetails := ""
|
||||||
|
if doctorVerbose {
|
||||||
|
dmsCliDetails = dmsCliPath
|
||||||
|
}
|
||||||
|
|
||||||
|
results := []checkResult{
|
||||||
|
{catVersions, "DMS CLI", statusOK, formatVersion(Version), dmsCliDetails, doctorDocsURL + "#dms-cli"},
|
||||||
|
}
|
||||||
|
|
||||||
|
qsVersion, qsStatus, qsPath := getQuickshellVersionInfo(qsMissingFeatures)
|
||||||
|
qsDetails := ""
|
||||||
|
if doctorVerbose && qsPath != "" {
|
||||||
|
qsDetails = qsPath
|
||||||
|
}
|
||||||
|
results = append(results, checkResult{catVersions, "Quickshell", qsStatus, qsVersion, qsDetails, doctorDocsURL + "#quickshell"})
|
||||||
|
|
||||||
|
dmsVersion, dmsPath := getDMSShellVersion()
|
||||||
|
if dmsVersion != "" {
|
||||||
|
results = append(results, checkResult{catVersions, "DMS Shell", statusOK, dmsVersion, dmsPath, doctorDocsURL + "#dms-shell"})
|
||||||
|
} else {
|
||||||
|
results = append(results, checkResult{catVersions, "DMS Shell", statusError, "Not installed or not detected", "Run 'dms setup' to install", doctorDocsURL + "#dms-shell"})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDMSShellVersion() (version, path string) {
|
||||||
|
if err := findConfig(nil, nil); err == nil && configPath != "" {
|
||||||
|
versionFile := filepath.Join(configPath, "VERSION")
|
||||||
|
if data, err := os.ReadFile(versionFile); err == nil {
|
||||||
|
return strings.TrimSpace(string(data)), configPath
|
||||||
|
}
|
||||||
|
return "installed", configPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if dmsPath, err := config.LocateDMSConfig(); err == nil {
|
||||||
|
versionFile := filepath.Join(dmsPath, "VERSION")
|
||||||
|
if data, err := os.ReadFile(versionFile); err == nil {
|
||||||
|
return strings.TrimSpace(string(data)), dmsPath
|
||||||
|
}
|
||||||
|
return "installed", dmsPath
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func getQuickshellVersionInfo(missingFeatures bool) (string, status, string) {
|
||||||
|
if !utils.CommandExists("qs") {
|
||||||
|
return "Not installed", statusError, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
qsPath, _ := exec.LookPath("qs")
|
||||||
|
|
||||||
|
output, err := exec.Command("qs", "--version").Output()
|
||||||
|
if err != nil {
|
||||||
|
return "Installed (version check failed)", statusWarn, qsPath
|
||||||
|
}
|
||||||
|
|
||||||
|
fullVersion := strings.TrimSpace(string(output))
|
||||||
|
if matches := quickshellVersionRegex.FindStringSubmatch(fullVersion); len(matches) >= 2 {
|
||||||
|
if version.CompareVersions(matches[1], "0.2.0") < 0 {
|
||||||
|
return fmt.Sprintf("%s (needs >= 0.2.0)", fullVersion), statusError, qsPath
|
||||||
|
}
|
||||||
|
if missingFeatures {
|
||||||
|
return fullVersion, statusWarn, qsPath
|
||||||
|
}
|
||||||
|
return fullVersion, statusOK, qsPath
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullVersion, statusWarn, qsPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkDMSInstallation() []checkResult {
|
||||||
|
var results []checkResult
|
||||||
|
|
||||||
|
dmsPath := ""
|
||||||
|
if err := findConfig(nil, nil); err == nil && configPath != "" {
|
||||||
|
dmsPath = configPath
|
||||||
|
} else if path, err := config.LocateDMSConfig(); err == nil {
|
||||||
|
dmsPath = path
|
||||||
|
}
|
||||||
|
|
||||||
|
if dmsPath == "" {
|
||||||
|
return []checkResult{{catInstallation, "DMS Configuration", statusError, "Not found", "shell.qml not found in any config path", doctorDocsURL + "#dms-configuration"}}
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, checkResult{catInstallation, "DMS Configuration", statusOK, "Found", dmsPath, doctorDocsURL + "#dms-configuration"})
|
||||||
|
|
||||||
|
shellQml := filepath.Join(dmsPath, "shell.qml")
|
||||||
|
if _, err := os.Stat(shellQml); err != nil {
|
||||||
|
results = append(results, checkResult{catInstallation, "shell.qml", statusError, "Missing", shellQml, doctorDocsURL + "#dms-configuration"})
|
||||||
|
} else {
|
||||||
|
results = append(results, checkResult{catInstallation, "shell.qml", statusOK, "Present", shellQml, doctorDocsURL + "#dms-configuration"})
|
||||||
|
}
|
||||||
|
|
||||||
|
if doctorVerbose {
|
||||||
|
installType := "Unknown"
|
||||||
|
switch {
|
||||||
|
case strings.Contains(dmsPath, "/nix/store"):
|
||||||
|
installType = "Nix store"
|
||||||
|
case strings.Contains(dmsPath, ".local/share") || strings.Contains(dmsPath, "/usr/share"):
|
||||||
|
installType = "System package"
|
||||||
|
case strings.Contains(dmsPath, ".config"):
|
||||||
|
installType = "User config"
|
||||||
|
}
|
||||||
|
results = append(results, checkResult{catInstallation, "Install Type", statusInfo, installType, dmsPath, doctorDocsURL + "#dms-configuration"})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkWindowManagers() []checkResult {
|
||||||
|
compositors := []struct {
|
||||||
|
name, versionCmd, versionArg string
|
||||||
|
versionRegex *regexp.Regexp
|
||||||
|
commands []string
|
||||||
|
}{
|
||||||
|
{"Hyprland", "hyprctl", "version", hyprlandVersionRegex, []string{"hyprland", "Hyprland"}},
|
||||||
|
{"niri", "niri", "--version", niriVersionRegex, []string{"niri"}},
|
||||||
|
{"Sway", "sway", "--version", swayVersionRegex, []string{"sway"}},
|
||||||
|
{"River", "river", "-version", riverVersionRegex, []string{"river"}},
|
||||||
|
{"Wayfire", "wayfire", "--version", wayfireVersionRegex, []string{"wayfire"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []checkResult
|
||||||
|
foundAny := false
|
||||||
|
|
||||||
|
for _, c := range compositors {
|
||||||
|
if !slices.ContainsFunc(c.commands, utils.CommandExists) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
foundAny = true
|
||||||
|
var compositorPath string
|
||||||
|
for _, cmd := range c.commands {
|
||||||
|
if path, err := exec.LookPath(cmd); err == nil {
|
||||||
|
compositorPath = path
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
details := ""
|
||||||
|
if doctorVerbose && compositorPath != "" {
|
||||||
|
details = compositorPath
|
||||||
|
}
|
||||||
|
results = append(results, checkResult{
|
||||||
|
catCompositor, c.name, statusOK,
|
||||||
|
getVersionFromCommand(c.versionCmd, c.versionArg, c.versionRegex), details,
|
||||||
|
doctorDocsURL + "#compositor",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundAny {
|
||||||
|
results = append(results, checkResult{
|
||||||
|
catCompositor, "Compositor", statusError,
|
||||||
|
"No supported Wayland compositor found",
|
||||||
|
"Install Hyprland, niri, Sway, River, or Wayfire",
|
||||||
|
doctorDocsURL + "#compositor",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if wm := detectRunningWM(); wm != "" {
|
||||||
|
results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, "", doctorDocsURL + "#compositor"})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string {
|
||||||
|
output, err := exec.Command(cmd, arg).Output()
|
||||||
|
if err != nil {
|
||||||
|
return "installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
outStr := string(output)
|
||||||
|
if matches := regex.FindStringSubmatch(outStr); len(matches) > 1 {
|
||||||
|
ver := matches[1]
|
||||||
|
if strings.Contains(outStr, "git") || strings.Contains(outStr, "dirty") {
|
||||||
|
return ver + " (git)"
|
||||||
|
}
|
||||||
|
return ver
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(outStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectRunningWM() string {
|
||||||
|
switch {
|
||||||
|
case os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "":
|
||||||
|
return "Hyprland"
|
||||||
|
case os.Getenv("NIRI_SOCKET") != "":
|
||||||
|
return "niri"
|
||||||
|
case os.Getenv("XDG_CURRENT_DESKTOP") != "":
|
||||||
|
return os.Getenv("XDG_CURRENT_DESKTOP")
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkQuickshellFeatures() ([]checkResult, bool) {
|
||||||
|
if !utils.CommandExists("qs") {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpDir := os.TempDir()
|
||||||
|
testScript := filepath.Join(tmpDir, "qs-feature-test.qml")
|
||||||
|
defer os.Remove(testScript)
|
||||||
|
|
||||||
|
qmlContent := `
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
ShellRoot {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool polkitAvailable: false
|
||||||
|
property bool idleMonitorAvailable: false
|
||||||
|
property bool idleInhibitorAvailable: false
|
||||||
|
property bool shortcutInhibitorAvailable: false
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 50
|
||||||
|
running: true
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
try {
|
||||||
|
var polkitTest = Qt.createQmlObject(
|
||||||
|
'import Quickshell.Services.Polkit; import QtQuick; Item {}',
|
||||||
|
root
|
||||||
|
)
|
||||||
|
root.polkitAvailable = true
|
||||||
|
polkitTest.destroy()
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var testItem = Qt.createQmlObject(
|
||||||
|
'import Quickshell.Wayland; import QtQuick; QtObject { ' +
|
||||||
|
'readonly property bool hasIdleMonitor: typeof IdleMonitor !== "undefined"; ' +
|
||||||
|
'readonly property bool hasIdleInhibitor: typeof IdleInhibitor !== "undefined"; ' +
|
||||||
|
'readonly property bool hasShortcutInhibitor: typeof ShortcutInhibitor !== "undefined" ' +
|
||||||
|
'}',
|
||||||
|
root
|
||||||
|
)
|
||||||
|
root.idleMonitorAvailable = testItem.hasIdleMonitor
|
||||||
|
root.idleInhibitorAvailable = testItem.hasIdleInhibitor
|
||||||
|
root.shortcutInhibitorAvailable = testItem.hasShortcutInhibitor
|
||||||
|
testItem.destroy()
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
console.warn(root.polkitAvailable ? "FEATURE:Polkit:OK" : "FEATURE:Polkit:UNAVAILABLE")
|
||||||
|
console.warn(root.idleMonitorAvailable ? "FEATURE:IdleMonitor:OK" : "FEATURE:IdleMonitor:UNAVAILABLE")
|
||||||
|
console.warn(root.idleInhibitorAvailable ? "FEATURE:IdleInhibitor:OK" : "FEATURE:IdleInhibitor:UNAVAILABLE")
|
||||||
|
console.warn(root.shortcutInhibitorAvailable ? "FEATURE:ShortcutInhibitor:OK" : "FEATURE:ShortcutInhibitor:UNAVAILABLE")
|
||||||
|
|
||||||
|
Quickshell.execDetached(["kill", "-TERM", String(Quickshell.processId)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
if err := os.WriteFile(testScript, []byte(qmlContent), 0644); err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("qs", "-p", testScript)
|
||||||
|
cmd.Env = append(os.Environ(), "NO_COLOR=1")
|
||||||
|
output, _ := cmd.CombinedOutput()
|
||||||
|
outputStr := string(output)
|
||||||
|
|
||||||
|
features := []struct{ name, desc string }{
|
||||||
|
{"Polkit", "Authentication prompts"},
|
||||||
|
{"IdleMonitor", "Idle detection"},
|
||||||
|
{"IdleInhibitor", "Prevent idle/sleep"},
|
||||||
|
{"ShortcutInhibitor", "Allow shortcut management (niri)"},
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []checkResult
|
||||||
|
missingFeatures := false
|
||||||
|
|
||||||
|
for _, f := range features {
|
||||||
|
available := strings.Contains(outputStr, fmt.Sprintf("FEATURE:%s:OK", f.name))
|
||||||
|
status, message := statusOK, "Available"
|
||||||
|
if !available {
|
||||||
|
status, message = statusInfo, "Not available"
|
||||||
|
missingFeatures = true
|
||||||
|
}
|
||||||
|
results = append(results, checkResult{catQuickshellFeatures, f.name, status, message, f.desc, doctorDocsURL + "#quickshell-features"})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, missingFeatures
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkI2CAvailability() checkResult {
|
||||||
|
ddc, err := brightness.NewDDCBackend()
|
||||||
|
if err != nil {
|
||||||
|
return checkResult{catOptionalFeatures, "I2C/DDC", statusInfo, "Not available", "External monitor brightness control", doctorDocsURL + "#optional-features"}
|
||||||
|
}
|
||||||
|
defer ddc.Close()
|
||||||
|
|
||||||
|
devices, err := ddc.GetDevices()
|
||||||
|
if err != nil || len(devices) == 0 {
|
||||||
|
return checkResult{catOptionalFeatures, "I2C/DDC", statusInfo, "No monitors detected", "External monitor brightness control", doctorDocsURL + "#optional-features"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return checkResult{catOptionalFeatures, "I2C/DDC", statusOK, fmt.Sprintf("%d monitor(s) detected", len(devices)), "External monitor brightness control", doctorDocsURL + "#optional-features"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectNetworkBackend() string {
|
||||||
|
result, err := network.DetectNetworkStack()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
switch result.Backend {
|
||||||
|
case network.BackendNetworkManager:
|
||||||
|
return "NetworkManager"
|
||||||
|
case network.BackendIwd:
|
||||||
|
return "iwd"
|
||||||
|
case network.BackendNetworkd:
|
||||||
|
if result.HasIwd {
|
||||||
|
return "iwd + systemd-networkd"
|
||||||
|
}
|
||||||
|
return "systemd-networkd"
|
||||||
|
case network.BackendConnMan:
|
||||||
|
return "ConnMan"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkOptionalDependencies() []checkResult {
|
||||||
|
var results []checkResult
|
||||||
|
|
||||||
|
if utils.IsServiceActive("accounts-daemon", false) {
|
||||||
|
results = append(results, checkResult{catOptionalFeatures, "accountsservice", statusOK, "Running", "User accounts", doctorDocsURL + "#optional-features"})
|
||||||
|
} else {
|
||||||
|
results = append(results, checkResult{catOptionalFeatures, "accountsservice", statusWarn, "Not running", "User accounts", doctorDocsURL + "#optional-features"})
|
||||||
|
}
|
||||||
|
|
||||||
|
if utils.IsServiceActive("power-profiles-daemon", false) {
|
||||||
|
results = append(results, checkResult{catOptionalFeatures, "power-profiles-daemon", statusOK, "Running", "Power profile management", doctorDocsURL + "#optional-features"})
|
||||||
|
} else {
|
||||||
|
results = append(results, checkResult{catOptionalFeatures, "power-profiles-daemon", statusInfo, "Not running", "Power profile management", doctorDocsURL + "#optional-features"})
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, checkI2CAvailability())
|
||||||
|
|
||||||
|
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
||||||
|
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
|
||||||
|
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", doctorDocsURL + "#optional-features"})
|
||||||
|
} else {
|
||||||
|
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty", doctorDocsURL + "#optional-features"})
|
||||||
|
}
|
||||||
|
|
||||||
|
deps := []struct {
|
||||||
|
name, cmd, altCmd, desc string
|
||||||
|
important bool
|
||||||
|
}{
|
||||||
|
{"matugen", "matugen", "", "Dynamic theming", true},
|
||||||
|
{"dgop", "dgop", "", "System monitoring", true},
|
||||||
|
{"cava", "cava", "", "Audio visualizer", true},
|
||||||
|
{"khal", "khal", "", "Calendar events", false},
|
||||||
|
{"Network", "nmcli", "iwctl", "Network management", false},
|
||||||
|
{"danksearch", "dsearch", "", "File search", false},
|
||||||
|
{"loginctl", "loginctl", "", "Session management", false},
|
||||||
|
{"fprintd", "fprintd-list", "", "Fingerprint auth", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range deps {
|
||||||
|
found, foundCmd := utils.CommandExists(d.cmd), d.cmd
|
||||||
|
if !found && d.altCmd != "" && utils.CommandExists(d.altCmd) {
|
||||||
|
found, foundCmd = true, d.altCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case found:
|
||||||
|
message := "Installed"
|
||||||
|
details := d.desc
|
||||||
|
if d.name == "Network" {
|
||||||
|
result, err := network.DetectNetworkStack()
|
||||||
|
if err == nil && result.Backend != network.BackendNone {
|
||||||
|
message = detectNetworkBackend() + " (active)"
|
||||||
|
if doctorVerbose {
|
||||||
|
details = result.ChosenReason
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch foundCmd {
|
||||||
|
case "nmcli":
|
||||||
|
message = "NetworkManager (installed)"
|
||||||
|
case "iwctl":
|
||||||
|
message = "iwd (installed)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results = append(results, checkResult{catOptionalFeatures, d.name, statusOK, message, details, doctorDocsURL + "#optional-features"})
|
||||||
|
case d.important:
|
||||||
|
results = append(results, checkResult{catOptionalFeatures, d.name, statusWarn, "Missing", d.desc, doctorDocsURL + "#optional-features"})
|
||||||
|
default:
|
||||||
|
results = append(results, checkResult{catOptionalFeatures, d.name, statusInfo, "Not installed", d.desc, doctorDocsURL + "#optional-features"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkConfigurationFiles() []checkResult {
|
||||||
|
configDir, _ := os.UserConfigDir()
|
||||||
|
cacheDir, _ := os.UserCacheDir()
|
||||||
|
dmsDir := "DankMaterialShell"
|
||||||
|
|
||||||
|
configFiles := []struct{ name, path string }{
|
||||||
|
{"settings.json", filepath.Join(configDir, dmsDir, "settings.json")},
|
||||||
|
{"clsettings.json", filepath.Join(configDir, dmsDir, "clsettings.json")},
|
||||||
|
{"plugin_settings.json", filepath.Join(configDir, dmsDir, "plugin_settings.json")},
|
||||||
|
{"session.json", filepath.Join(utils.XDGStateHome(), dmsDir, "session.json")},
|
||||||
|
{"dms-colors.json", filepath.Join(cacheDir, dmsDir, "dms-colors.json")},
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []checkResult
|
||||||
|
for _, cf := range configFiles {
|
||||||
|
info, err := os.Stat(cf.path)
|
||||||
|
if err != nil {
|
||||||
|
results = append(results, checkResult{catConfigFiles, cf.name, statusInfo, "Not yet created", cf.path, doctorDocsURL + "#config-files"})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
status := statusOK
|
||||||
|
message := "Present"
|
||||||
|
if info.Mode().Perm()&0200 == 0 {
|
||||||
|
status = statusWarn
|
||||||
|
message += " (read-only)"
|
||||||
|
}
|
||||||
|
results = append(results, checkResult{catConfigFiles, cf.name, status, message, cf.path, doctorDocsURL + "#config-files"})
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkSystemdServices() []checkResult {
|
||||||
|
if !utils.CommandExists("systemctl") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []checkResult
|
||||||
|
|
||||||
|
dmsState := getServiceState("dms", true)
|
||||||
|
if !dmsState.exists {
|
||||||
|
results = append(results, checkResult{catServices, "dms.service", statusInfo, "Not installed", "Optional user service", doctorDocsURL + "#services"})
|
||||||
|
} else {
|
||||||
|
status, message := statusOK, dmsState.enabled
|
||||||
|
if dmsState.active != "" {
|
||||||
|
message = fmt.Sprintf("%s, %s", dmsState.enabled, dmsState.active)
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case dmsState.enabled == "disabled":
|
||||||
|
status, message = statusWarn, "Disabled"
|
||||||
|
case dmsState.active == "failed" || dmsState.active == "inactive":
|
||||||
|
status = statusError
|
||||||
|
}
|
||||||
|
results = append(results, checkResult{catServices, "dms.service", status, message, "", doctorDocsURL + "#services"})
|
||||||
|
}
|
||||||
|
|
||||||
|
greetdState := getServiceState("greetd", false)
|
||||||
|
switch {
|
||||||
|
case greetdState.exists:
|
||||||
|
status := statusOK
|
||||||
|
if greetdState.enabled == "disabled" {
|
||||||
|
status = statusInfo
|
||||||
|
}
|
||||||
|
results = append(results, checkResult{catServices, "greetd", status, greetdState.enabled, "", doctorDocsURL + "#services"})
|
||||||
|
case doctorVerbose:
|
||||||
|
results = append(results, checkResult{catServices, "greetd", statusInfo, "Not installed", "Optional greeter service", doctorDocsURL + "#services"})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
type serviceState struct {
|
||||||
|
exists bool
|
||||||
|
enabled string
|
||||||
|
active string
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServiceState(name string, userService bool) serviceState {
|
||||||
|
args := []string{"is-enabled", name}
|
||||||
|
if userService {
|
||||||
|
args = []string{"--user", "is-enabled", name}
|
||||||
|
}
|
||||||
|
|
||||||
|
output, _ := exec.Command("systemctl", args...).Output()
|
||||||
|
enabled := strings.TrimSpace(string(output))
|
||||||
|
|
||||||
|
if enabled == "" || enabled == "not-found" {
|
||||||
|
return serviceState{}
|
||||||
|
}
|
||||||
|
|
||||||
|
state := serviceState{exists: true, enabled: enabled}
|
||||||
|
|
||||||
|
if userService {
|
||||||
|
output, _ = exec.Command("systemctl", "--user", "is-active", name).Output()
|
||||||
|
if active := strings.TrimSpace(string(output)); active != "" && active != "unknown" {
|
||||||
|
state.active = active
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
func printResults(results []checkResult) {
|
||||||
|
theme := tui.TerminalTheme()
|
||||||
|
styles := tui.NewStyles(theme)
|
||||||
|
|
||||||
|
currentCategory := category(-1)
|
||||||
|
for _, r := range results {
|
||||||
|
if r.category != currentCategory {
|
||||||
|
if currentCategory != -1 {
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
fmt.Printf(" %s\n", styles.Bold.Render(r.category.String()))
|
||||||
|
currentCategory = r.category
|
||||||
|
}
|
||||||
|
printResultLine(r, styles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printResultsJSON(results []checkResult) {
|
||||||
|
var ds DoctorStatus
|
||||||
|
for _, r := range results {
|
||||||
|
ds.Add(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
output := doctorOutputJSON{}
|
||||||
|
output.Summary.Errors = ds.ErrorCount()
|
||||||
|
output.Summary.Warnings = ds.WarningCount()
|
||||||
|
output.Summary.OK = ds.OKCount()
|
||||||
|
output.Summary.Info = len(ds.Info)
|
||||||
|
|
||||||
|
output.Results = make([]checkResultJSON, 0, len(results))
|
||||||
|
for _, r := range results {
|
||||||
|
output.Results = append(output.Results, r.toJSON())
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(os.Stdout)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
if err := encoder.Encode(output); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error encoding JSON: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printResultLine(r checkResult, styles tui.Styles) {
|
||||||
|
icon, style := r.status.IconStyle(styles)
|
||||||
|
|
||||||
|
name := r.name
|
||||||
|
nameLen := len(name)
|
||||||
|
|
||||||
|
if nameLen > checkNameMaxLength {
|
||||||
|
name = name[:checkNameMaxLength-1] + "…"
|
||||||
|
nameLen = checkNameMaxLength
|
||||||
|
}
|
||||||
|
dots := strings.Repeat("·", checkNameMaxLength-nameLen)
|
||||||
|
|
||||||
|
fmt.Printf(" %s %s %s %s\n", style.Render(icon), name, styles.Subtle.Render(dots), r.message)
|
||||||
|
|
||||||
|
if doctorVerbose && r.details != "" {
|
||||||
|
fmt.Printf(" %s\n", styles.Subtle.Render("└─ "+r.details))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printSummary(results []checkResult, qsMissingFeatures bool) {
|
||||||
|
theme := tui.TerminalTheme()
|
||||||
|
styles := tui.NewStyles(theme)
|
||||||
|
|
||||||
|
var ds DoctorStatus
|
||||||
|
for _, r := range results {
|
||||||
|
ds.Add(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf(" %s\n", styles.Subtle.Render("──────────────────────────────────────"))
|
||||||
|
|
||||||
|
if !ds.HasIssues() {
|
||||||
|
fmt.Printf(" %s\n", styles.Success.Render("✓ All checks passed!"))
|
||||||
|
} else {
|
||||||
|
var parts []string
|
||||||
|
|
||||||
|
if ds.ErrorCount() > 0 {
|
||||||
|
parts = append(parts, styles.Error.Render(fmt.Sprintf("%d error(s)", ds.ErrorCount())))
|
||||||
|
}
|
||||||
|
if ds.WarningCount() > 0 {
|
||||||
|
parts = append(parts, styles.Warning.Render(fmt.Sprintf("%d warning(s)", ds.WarningCount())))
|
||||||
|
}
|
||||||
|
parts = append(parts, styles.Success.Render(fmt.Sprintf("%d ok", ds.OKCount())))
|
||||||
|
fmt.Printf(" %s\n", strings.Join(parts, ", "))
|
||||||
|
|
||||||
|
if qsMissingFeatures {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf(" %s\n", styles.Subtle.Render("→ Consider using quickshell-git for full feature support"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
@@ -377,7 +377,7 @@ func updateDMSBinary() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
version := ""
|
version := ""
|
||||||
for _, line := range strings.Split(string(output), "\n") {
|
for line := range strings.SplitSeq(string(output), "\n") {
|
||||||
if strings.Contains(line, "\"tag_name\"") {
|
if strings.Contains(line, "\"tag_name\"") {
|
||||||
parts := strings.Split(line, "\"")
|
parts := strings.Split(line, "\"")
|
||||||
if len(parts) >= 4 {
|
if len(parts) >= 4 {
|
||||||
@@ -443,7 +443,7 @@ func updateDMSBinary() error {
|
|||||||
|
|
||||||
decompressedPath := filepath.Join(tempDir, "dms")
|
decompressedPath := filepath.Join(tempDir, "dms")
|
||||||
|
|
||||||
if err := os.Chmod(decompressedPath, 0755); err != nil {
|
if err := os.Chmod(decompressedPath, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to make binary executable: %w", err)
|
return fmt.Errorf("failed to make binary executable: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -211,8 +211,8 @@ func checkGroupExists(groupName string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := strings.Split(string(data), "\n")
|
lines := strings.SplitSeq(string(data), "\n")
|
||||||
for _, line := range lines {
|
for line := range lines {
|
||||||
if strings.HasPrefix(line, groupName+":") {
|
if strings.HasPrefix(line, groupName+":") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -521,7 +521,7 @@ func enableGreeter() error {
|
|||||||
newConfig := strings.Join(finalLines, "\n")
|
newConfig := strings.Join(finalLines, "\n")
|
||||||
|
|
||||||
tmpFile := "/tmp/greetd-config.toml"
|
tmpFile := "/tmp/greetd-config.toml"
|
||||||
if err := os.WriteFile(tmpFile, []byte(newConfig), 0644); err != nil {
|
if err := os.WriteFile(tmpFile, []byte(newConfig), 0o644); err != nil {
|
||||||
return fmt.Errorf("failed to write temp config: %w", err)
|
return fmt.Errorf("failed to write temp config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,8 +592,8 @@ func checkGreeterStatus() error {
|
|||||||
if data, err := os.ReadFile(configPath); err == nil {
|
if data, err := os.ReadFile(configPath); err == nil {
|
||||||
configContent := string(data)
|
configContent := string(data)
|
||||||
if strings.Contains(configContent, "dms-greeter") {
|
if strings.Contains(configContent, "dms-greeter") {
|
||||||
lines := strings.Split(configContent, "\n")
|
lines := strings.SplitSeq(configContent, "\n")
|
||||||
for _, line := range lines {
|
for line := range lines {
|
||||||
trimmed := strings.TrimSpace(line)
|
trimmed := strings.TrimSpace(line)
|
||||||
if strings.HasPrefix(trimmed, "command =") || strings.HasPrefix(trimmed, "command=") {
|
if strings.HasPrefix(trimmed, "command =") || strings.HasPrefix(trimmed, "command=") {
|
||||||
parts := strings.SplitN(trimmed, "=", 2)
|
parts := strings.SplitN(trimmed, "=", 2)
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
|||||||
ConfigDir: configDir,
|
ConfigDir: configDir,
|
||||||
Kind: kind,
|
Kind: kind,
|
||||||
Value: value,
|
Value: value,
|
||||||
Mode: mode,
|
Mode: matugen.ColorMode(mode),
|
||||||
IconTheme: iconTheme,
|
IconTheme: iconTheme,
|
||||||
MatugenType: matugenType,
|
MatugenType: matugenType,
|
||||||
RunUserTemplates: runUserTemplates,
|
RunUserTemplates: runUserTemplates,
|
||||||
|
|||||||
@@ -131,6 +131,12 @@ func runOpen(target string) {
|
|||||||
detectedRequestType = "url"
|
detectedRequestType = "url"
|
||||||
}
|
}
|
||||||
log.Infof("Detected HTTP(S) URL")
|
log.Infof("Detected HTTP(S) URL")
|
||||||
|
} else if strings.HasPrefix(target, "dms://") {
|
||||||
|
// Handle DMS internal URLs (theme/plugin install, etc.)
|
||||||
|
if detectedRequestType == "" {
|
||||||
|
detectedRequestType = "url"
|
||||||
|
}
|
||||||
|
log.Infof("Detected DMS internal URL")
|
||||||
} else if _, err := os.Stat(target); err == nil {
|
} else if _, err := os.Stat(target); err == nil {
|
||||||
// Handle local file paths directly (not file:// URIs)
|
// Handle local file paths directly (not file:// URIs)
|
||||||
// Convert to absolute path
|
// Convert to absolute path
|
||||||
@@ -177,7 +183,7 @@ func runOpen(target string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
method := "apppicker.open"
|
method := "apppicker.open"
|
||||||
if detectedMimeType == "" && len(detectedCategories) == 0 && (strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://")) {
|
if detectedMimeType == "" && len(detectedCategories) == 0 && (strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") || strings.HasPrefix(target, "dms://")) {
|
||||||
method = "browser.open"
|
method = "browser.open"
|
||||||
params["url"] = target
|
params["url"] = target
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,15 +50,18 @@ func findConfig(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
||||||
if data, readErr := os.ReadFile(configStateFile); readErr == nil {
|
if data, readErr := os.ReadFile(configStateFile); readErr == nil {
|
||||||
statePath := strings.TrimSpace(string(data))
|
if len(getAllDMSPIDs()) == 0 {
|
||||||
shellPath := filepath.Join(statePath, "shell.qml")
|
os.Remove(configStateFile)
|
||||||
|
|
||||||
if info, statErr := os.Stat(shellPath); statErr == nil && !info.IsDir() {
|
|
||||||
log.Debug("Using config from active session state file: %s", statePath)
|
|
||||||
configPath = statePath
|
|
||||||
log.Debug("Using config from: %s", configPath)
|
|
||||||
return nil // <-- Guard statement
|
|
||||||
} else {
|
} else {
|
||||||
|
statePath := strings.TrimSpace(string(data))
|
||||||
|
shellPath := filepath.Join(statePath, "shell.qml")
|
||||||
|
|
||||||
|
if info, statErr := os.Stat(shellPath); statErr == nil && !info.IsDir() {
|
||||||
|
log.Debug("Using config from active session state file: %s", statePath)
|
||||||
|
configPath = statePath
|
||||||
|
log.Debug("Using config from: %s", configPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
os.Remove(configStateFile)
|
os.Remove(configStateFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,20 +87,14 @@ func newDPMSClient() (*dpmsClient, error) {
|
|||||||
switch e.Interface {
|
switch e.Interface {
|
||||||
case wlr_output_power.ZwlrOutputPowerManagerV1InterfaceName:
|
case wlr_output_power.ZwlrOutputPowerManagerV1InterfaceName:
|
||||||
powerMgr := wlr_output_power.NewZwlrOutputPowerManagerV1(c.ctx)
|
powerMgr := wlr_output_power.NewZwlrOutputPowerManagerV1(c.ctx)
|
||||||
version := e.Version
|
version := min(e.Version, 1)
|
||||||
if version > 1 {
|
|
||||||
version = 1
|
|
||||||
}
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, version, powerMgr); err == nil {
|
if err := registry.Bind(e.Name, e.Interface, version, powerMgr); err == nil {
|
||||||
c.powerMgr = powerMgr
|
c.powerMgr = powerMgr
|
||||||
}
|
}
|
||||||
|
|
||||||
case "wl_output":
|
case "wl_output":
|
||||||
output := wlclient.NewOutput(c.ctx)
|
output := wlclient.NewOutput(c.ctx)
|
||||||
version := e.Version
|
version := min(e.Version, 4)
|
||||||
if version > 4 {
|
|
||||||
version = 4
|
|
||||||
}
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, version, output); err == nil {
|
if err := registry.Bind(e.Name, e.Interface, version, output); err == nil {
|
||||||
outputID := fmt.Sprintf("output-%d", output.ID())
|
outputID := fmt.Sprintf("output-%d", output.ID())
|
||||||
state := &outputState{
|
state := &outputState{
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -18,6 +20,25 @@ import (
|
|||||||
|
|
||||||
type ipcTargets map[string]map[string][]string
|
type ipcTargets map[string]map[string][]string
|
||||||
|
|
||||||
|
// getProcessExitCode returns the exit code from a ProcessState.
|
||||||
|
// For normal exits, returns the exit code directly.
|
||||||
|
// For signal termination, returns 128 + signal number (Unix convention).
|
||||||
|
func getProcessExitCode(state *os.ProcessState) int {
|
||||||
|
if state == nil {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if code := state.ExitCode(); code != -1 {
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
// Process was killed by signal - extract signal number
|
||||||
|
if status, ok := state.Sys().(syscall.WaitStatus); ok {
|
||||||
|
if status.Signaled() {
|
||||||
|
return 128 + int(status.Signal())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
var isSessionManaged bool
|
var isSessionManaged bool
|
||||||
|
|
||||||
func execDetachedRestart(targetPID int) {
|
func execDetachedRestart(targetPID int) {
|
||||||
@@ -165,8 +186,10 @@ func runShellInteractive(session bool) {
|
|||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
|
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
|
||||||
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
|
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
|
||||||
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
|
if os.Getenv("QT_LOGGING_RULES") == "" {
|
||||||
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
|
||||||
|
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if isSessionManaged && hasSystemdRun() {
|
if isSessionManaged && hasSystemdRun() {
|
||||||
@@ -180,6 +203,16 @@ func runShellInteractive(session bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if os.Getenv("QT_QPA_PLATFORMTHEME") == "" {
|
||||||
|
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3")
|
||||||
|
}
|
||||||
|
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" {
|
||||||
|
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
|
||||||
|
}
|
||||||
|
if os.Getenv("QT_QPA_PLATFORM") == "" {
|
||||||
|
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland")
|
||||||
|
}
|
||||||
|
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
@@ -214,14 +247,28 @@ func runShellInteractive(session bool) {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case sig := <-sigChan:
|
case sig := <-sigChan:
|
||||||
// Handle SIGUSR1 restart for non-session managed processes
|
if sig == syscall.SIGUSR1 {
|
||||||
if sig == syscall.SIGUSR1 && !isSessionManaged {
|
if isSessionManaged {
|
||||||
|
log.Infof("Received SIGUSR1, exiting for systemd restart...")
|
||||||
|
cancel()
|
||||||
|
cmd.Process.Signal(syscall.SIGTERM)
|
||||||
|
os.Remove(socketPath)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
log.Infof("Received SIGUSR1, spawning detached restart process...")
|
log.Infof("Received SIGUSR1, spawning detached restart process...")
|
||||||
execDetachedRestart(os.Getpid())
|
execDetachedRestart(os.Getpid())
|
||||||
// Exit immediately to avoid race conditions with detached restart
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released)
|
||||||
|
select {
|
||||||
|
case <-errChan:
|
||||||
|
cancel()
|
||||||
|
os.Remove(socketPath)
|
||||||
|
os.Exit(getProcessExitCode(cmd.ProcessState))
|
||||||
|
case <-time.After(500 * time.Millisecond):
|
||||||
|
}
|
||||||
|
|
||||||
log.Infof("\nReceived signal %v, shutting down...", sig)
|
log.Infof("\nReceived signal %v, shutting down...", sig)
|
||||||
cancel()
|
cancel()
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
cmd.Process.Signal(syscall.SIGTERM)
|
||||||
@@ -235,7 +282,7 @@ func runShellInteractive(session bool) {
|
|||||||
cmd.Process.Signal(syscall.SIGTERM)
|
cmd.Process.Signal(syscall.SIGTERM)
|
||||||
}
|
}
|
||||||
os.Remove(socketPath)
|
os.Remove(socketPath)
|
||||||
os.Exit(1)
|
os.Exit(getProcessExitCode(cmd.ProcessState))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -328,13 +375,7 @@ func killShell() {
|
|||||||
|
|
||||||
func runShellDaemon(session bool) {
|
func runShellDaemon(session bool) {
|
||||||
isSessionManaged = session
|
isSessionManaged = session
|
||||||
isDaemonChild := false
|
isDaemonChild := slices.Contains(os.Args, "--daemon-child")
|
||||||
for _, arg := range os.Args {
|
|
||||||
if arg == "--daemon-child" {
|
|
||||||
isDaemonChild = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isDaemonChild {
|
if !isDaemonChild {
|
||||||
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
|
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
|
||||||
@@ -385,8 +426,10 @@ func runShellDaemon(session bool) {
|
|||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
|
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
|
||||||
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
|
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
|
||||||
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
|
if os.Getenv("QT_LOGGING_RULES") == "" {
|
||||||
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
|
||||||
|
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if isSessionManaged && hasSystemdRun() {
|
if isSessionManaged && hasSystemdRun() {
|
||||||
@@ -400,6 +443,16 @@ func runShellDaemon(session bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if os.Getenv("QT_QPA_PLATFORMTHEME") == "" {
|
||||||
|
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3")
|
||||||
|
}
|
||||||
|
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" {
|
||||||
|
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
|
||||||
|
}
|
||||||
|
if os.Getenv("QT_QPA_PLATFORM") == "" {
|
||||||
|
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland")
|
||||||
|
}
|
||||||
|
|
||||||
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
|
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error opening /dev/null: %v", err)
|
log.Fatalf("Error opening /dev/null: %v", err)
|
||||||
@@ -440,15 +493,28 @@ func runShellDaemon(session bool) {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case sig := <-sigChan:
|
case sig := <-sigChan:
|
||||||
// Handle SIGUSR1 restart for non-session managed processes
|
if sig == syscall.SIGUSR1 {
|
||||||
if sig == syscall.SIGUSR1 && !isSessionManaged {
|
if isSessionManaged {
|
||||||
|
log.Infof("Received SIGUSR1, exiting for systemd restart...")
|
||||||
|
cancel()
|
||||||
|
cmd.Process.Signal(syscall.SIGTERM)
|
||||||
|
os.Remove(socketPath)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
log.Infof("Received SIGUSR1, spawning detached restart process...")
|
log.Infof("Received SIGUSR1, spawning detached restart process...")
|
||||||
execDetachedRestart(os.Getpid())
|
execDetachedRestart(os.Getpid())
|
||||||
// Exit immediately to avoid race conditions with detached restart
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// All other signals: clean shutdown
|
// Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released)
|
||||||
|
select {
|
||||||
|
case <-errChan:
|
||||||
|
cancel()
|
||||||
|
os.Remove(socketPath)
|
||||||
|
os.Exit(getProcessExitCode(cmd.ProcessState))
|
||||||
|
case <-time.After(500 * time.Millisecond):
|
||||||
|
}
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
cmd.Process.Signal(syscall.SIGTERM)
|
||||||
os.Remove(socketPath)
|
os.Remove(socketPath)
|
||||||
@@ -460,17 +526,25 @@ func runShellDaemon(session bool) {
|
|||||||
cmd.Process.Signal(syscall.SIGTERM)
|
cmd.Process.Signal(syscall.SIGTERM)
|
||||||
}
|
}
|
||||||
os.Remove(socketPath)
|
os.Remove(socketPath)
|
||||||
os.Exit(1)
|
os.Exit(getProcessExitCode(cmd.ProcessState))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var qsHasAnyDisplay = sync.OnceValue(func() bool {
|
||||||
|
out, err := exec.Command("qs", "ipc", "--help").Output()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.Contains(string(out), "--any-display")
|
||||||
|
})
|
||||||
|
|
||||||
func parseTargetsFromIPCShowOutput(output string) ipcTargets {
|
func parseTargetsFromIPCShowOutput(output string) ipcTargets {
|
||||||
targets := make(ipcTargets)
|
targets := make(ipcTargets)
|
||||||
var currentTarget string
|
var currentTarget string
|
||||||
for _, line := range strings.Split(output, "\n") {
|
for line := range strings.SplitSeq(output, "\n") {
|
||||||
if strings.HasPrefix(line, "target ") {
|
if after, ok := strings.CutPrefix(line, "target "); ok {
|
||||||
currentTarget = strings.TrimSpace(strings.TrimPrefix(line, "target "))
|
currentTarget = strings.TrimSpace(after)
|
||||||
targets[currentTarget] = make(map[string][]string)
|
targets[currentTarget] = make(map[string][]string)
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(line, " function") && currentTarget != "" {
|
if strings.HasPrefix(line, " function") && currentTarget != "" {
|
||||||
@@ -495,7 +569,11 @@ func parseTargetsFromIPCShowOutput(output string) ipcTargets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getShellIPCCompletions(args []string, _ string) []string {
|
func getShellIPCCompletions(args []string, _ string) []string {
|
||||||
cmdArgs := []string{"-p", configPath, "ipc", "show"}
|
cmdArgs := []string{"ipc"}
|
||||||
|
if qsHasAnyDisplay() {
|
||||||
|
cmdArgs = append(cmdArgs, "--any-display")
|
||||||
|
}
|
||||||
|
cmdArgs = append(cmdArgs, "-p", configPath, "show")
|
||||||
cmd := exec.Command("qs", cmdArgs...)
|
cmd := exec.Command("qs", cmdArgs...)
|
||||||
var targets ipcTargets
|
var targets ipcTargets
|
||||||
|
|
||||||
@@ -549,7 +627,12 @@ func runShellIPCCommand(args []string) {
|
|||||||
args = append([]string{"call"}, args...)
|
args = append([]string{"call"}, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdArgs := append([]string{"-p", configPath, "ipc"}, args...)
|
cmdArgs := []string{"ipc"}
|
||||||
|
if qsHasAnyDisplay() {
|
||||||
|
cmdArgs = append(cmdArgs, "--any-display")
|
||||||
|
}
|
||||||
|
cmdArgs = append(cmdArgs, "-p", configPath)
|
||||||
|
cmdArgs = append(cmdArgs, args...)
|
||||||
cmd := exec.Command("qs", cmdArgs...)
|
cmd := exec.Command("qs", cmdArgs...)
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,13 +37,7 @@ func checkSystemdServiceEnabled(serviceName string) (string, bool, error) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
knownStates := []string{"disabled", "masked", "masked-runtime", "not-found", "enabled", "enabled-runtime", "static", "indirect", "alias"}
|
knownStates := []string{"disabled", "masked", "masked-runtime", "not-found", "enabled", "enabled-runtime", "static", "indirect", "alias"}
|
||||||
isKnownState := false
|
isKnownState := slices.Contains(knownStates, stateStr)
|
||||||
for _, known := range knownStates {
|
|
||||||
if stateStr == known {
|
|
||||||
isKnownState = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isKnownState {
|
if !isKnownState {
|
||||||
return stateStr, false, fmt.Errorf("systemctl is-enabled failed: %w (output: %s)", err, stateStr)
|
return stateStr, false, fmt.Errorf("systemctl is-enabled failed: %w (output: %s)", err, stateStr)
|
||||||
|
|||||||
@@ -221,10 +221,7 @@ func (p *Picker) handleGlobal(e client.RegistryGlobalEvent) {
|
|||||||
|
|
||||||
case client.OutputInterfaceName:
|
case client.OutputInterfaceName:
|
||||||
output := client.NewOutput(p.ctx)
|
output := client.NewOutput(p.ctx)
|
||||||
version := e.Version
|
version := min(e.Version, 4)
|
||||||
if version > 4 {
|
|
||||||
version = 4
|
|
||||||
}
|
|
||||||
if err := p.registry.Bind(e.Name, e.Interface, version, output); err == nil {
|
if err := p.registry.Bind(e.Name, e.Interface, version, output); err == nil {
|
||||||
p.outputsMu.Lock()
|
p.outputsMu.Lock()
|
||||||
p.outputs[e.Name] = &Output{
|
p.outputs[e.Name] = &Output{
|
||||||
@@ -239,20 +236,14 @@ func (p *Picker) handleGlobal(e client.RegistryGlobalEvent) {
|
|||||||
|
|
||||||
case wlr_layer_shell.ZwlrLayerShellV1InterfaceName:
|
case wlr_layer_shell.ZwlrLayerShellV1InterfaceName:
|
||||||
layerShell := wlr_layer_shell.NewZwlrLayerShellV1(p.ctx)
|
layerShell := wlr_layer_shell.NewZwlrLayerShellV1(p.ctx)
|
||||||
version := e.Version
|
version := min(e.Version, 4)
|
||||||
if version > 4 {
|
|
||||||
version = 4
|
|
||||||
}
|
|
||||||
if err := p.registry.Bind(e.Name, e.Interface, version, layerShell); err == nil {
|
if err := p.registry.Bind(e.Name, e.Interface, version, layerShell); err == nil {
|
||||||
p.layerShell = layerShell
|
p.layerShell = layerShell
|
||||||
}
|
}
|
||||||
|
|
||||||
case wlr_screencopy.ZwlrScreencopyManagerV1InterfaceName:
|
case wlr_screencopy.ZwlrScreencopyManagerV1InterfaceName:
|
||||||
screencopy := wlr_screencopy.NewZwlrScreencopyManagerV1(p.ctx)
|
screencopy := wlr_screencopy.NewZwlrScreencopyManagerV1(p.ctx)
|
||||||
version := e.Version
|
version := min(e.Version, 3)
|
||||||
if version > 3 {
|
|
||||||
version = 3
|
|
||||||
}
|
|
||||||
if err := p.registry.Bind(e.Name, e.Interface, version, screencopy); err == nil {
|
if err := p.registry.Bind(e.Name, e.Interface, version, screencopy); err == nil {
|
||||||
p.screencopy = screencopy
|
p.screencopy = screencopy
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1157,7 +1157,7 @@ func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color,
|
|||||||
rOff, bOff = 2, 0
|
rOff, bOff = 2, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
for row := 0; row < fontH; row++ {
|
for row := range fontH {
|
||||||
yy := y + row
|
yy := y + row
|
||||||
if yy < 0 || yy >= height {
|
if yy < 0 || yy >= height {
|
||||||
continue
|
continue
|
||||||
@@ -1165,7 +1165,7 @@ func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color,
|
|||||||
rowPattern := g[row]
|
rowPattern := g[row]
|
||||||
dstRowOff := yy * stride
|
dstRowOff := yy * stride
|
||||||
|
|
||||||
for colIdx := 0; colIdx < fontW; colIdx++ {
|
for colIdx := range fontW {
|
||||||
if (rowPattern & (1 << (fontW - 1 - colIdx))) == 0 {
|
if (rowPattern & (1 << (fontW - 1 - colIdx))) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ func TestSurfaceState_ConcurrentPointerMotion(t *testing.T) {
|
|||||||
const goroutines = 50
|
const goroutines = 50
|
||||||
const iterations = 100
|
const iterations = 100
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
for i := range goroutines {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(id int) {
|
go func(id int) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for j := 0; j < iterations; j++ {
|
for j := range iterations {
|
||||||
s.OnPointerMotion(float64(id*10+j), float64(id*10+j))
|
s.OnPointerMotion(float64(id*10+j), float64(id*10+j))
|
||||||
}
|
}
|
||||||
}(i)
|
}(i)
|
||||||
@@ -34,21 +34,21 @@ func TestSurfaceState_ConcurrentScaleAccess(t *testing.T) {
|
|||||||
const goroutines = 30
|
const goroutines = 30
|
||||||
const iterations = 100
|
const iterations = 100
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
for i := range goroutines / 2 {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(id int) {
|
go func(id int) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for j := 0; j < iterations; j++ {
|
for range iterations {
|
||||||
s.SetScale(int32(id%3 + 1))
|
s.SetScale(int32(id%3 + 1))
|
||||||
}
|
}
|
||||||
}(i)
|
}(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
for range goroutines / 2 {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for j := 0; j < iterations; j++ {
|
for range iterations {
|
||||||
scale := s.Scale()
|
scale := s.Scale()
|
||||||
assert.GreaterOrEqual(t, scale, int32(1))
|
assert.GreaterOrEqual(t, scale, int32(1))
|
||||||
}
|
}
|
||||||
@@ -65,21 +65,21 @@ func TestSurfaceState_ConcurrentLogicalSize(t *testing.T) {
|
|||||||
const goroutines = 20
|
const goroutines = 20
|
||||||
const iterations = 100
|
const iterations = 100
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
for i := range goroutines / 2 {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(id int) {
|
go func(id int) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for j := 0; j < iterations; j++ {
|
for j := range iterations {
|
||||||
_ = s.OnLayerConfigure(1920+id, 1080+j)
|
_ = s.OnLayerConfigure(1920+id, 1080+j)
|
||||||
}
|
}
|
||||||
}(i)
|
}(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
for range goroutines / 2 {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for j := 0; j < iterations; j++ {
|
for range iterations {
|
||||||
w, h := s.LogicalSize()
|
w, h := s.LogicalSize()
|
||||||
_ = w
|
_ = w
|
||||||
_ = h
|
_ = h
|
||||||
@@ -97,31 +97,31 @@ func TestSurfaceState_ConcurrentIsDone(t *testing.T) {
|
|||||||
const goroutines = 30
|
const goroutines = 30
|
||||||
const iterations = 100
|
const iterations = 100
|
||||||
|
|
||||||
for i := 0; i < goroutines/3; i++ {
|
for range goroutines / 3 {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for j := 0; j < iterations; j++ {
|
for range iterations {
|
||||||
s.OnPointerButton(0x110, 1)
|
s.OnPointerButton(0x110, 1)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < goroutines/3; i++ {
|
for range goroutines / 3 {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for j := 0; j < iterations; j++ {
|
for range iterations {
|
||||||
s.OnKey(1, 1)
|
s.OnKey(1, 1)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < goroutines/3; i++ {
|
for range goroutines / 3 {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for j := 0; j < iterations; j++ {
|
for range iterations {
|
||||||
picked, cancelled := s.IsDone()
|
picked, cancelled := s.IsDone()
|
||||||
_ = picked
|
_ = picked
|
||||||
_ = cancelled
|
_ = cancelled
|
||||||
@@ -139,11 +139,11 @@ func TestSurfaceState_ConcurrentIsReady(t *testing.T) {
|
|||||||
const goroutines = 20
|
const goroutines = 20
|
||||||
const iterations = 100
|
const iterations = 100
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
for range goroutines {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for j := 0; j < iterations; j++ {
|
for range iterations {
|
||||||
_ = s.IsReady()
|
_ = s.IsReady()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -159,11 +159,11 @@ func TestSurfaceState_ConcurrentSwapBuffers(t *testing.T) {
|
|||||||
const goroutines = 20
|
const goroutines = 20
|
||||||
const iterations = 100
|
const iterations = 100
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
for range goroutines {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for j := 0; j < iterations; j++ {
|
for range iterations {
|
||||||
s.SwapBuffers()
|
s.SwapBuffers()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -213,6 +213,11 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
|
|||||||
|
|
||||||
for _, cfg := range configs {
|
for _, cfg := range configs {
|
||||||
path := filepath.Join(dmsDir, cfg.name)
|
path := filepath.Join(dmsDir, cfg.name)
|
||||||
|
// Skip if file already exists to preserve user modifications
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
if err := os.WriteFile(path, []byte(cfg.content), 0644); err != nil {
|
if err := os.WriteFile(path, []byte(cfg.content), 0644); err != nil {
|
||||||
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
|
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
|
||||||
}
|
}
|
||||||
@@ -265,7 +270,13 @@ func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
|
|||||||
|
|
||||||
colorResult := DeploymentResult{
|
colorResult := DeploymentResult{
|
||||||
ConfigType: "Ghostty Colors",
|
ConfigType: "Ghostty Colors",
|
||||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config-dankcolors"),
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "themes", "dankcolors"),
|
||||||
|
}
|
||||||
|
|
||||||
|
themesDir := filepath.Dir(colorResult.Path)
|
||||||
|
if err := os.MkdirAll(themesDir, 0755); err != nil {
|
||||||
|
mainResult.Error = fmt.Errorf("failed to create themes directory: %w", err)
|
||||||
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(colorResult.Path, []byte(GhosttyColorConfig), 0644); err != nil {
|
if err := os.WriteFile(colorResult.Path, []byte(GhosttyColorConfig), 0644); err != nil {
|
||||||
|
|||||||
@@ -462,13 +462,13 @@ func TestHyprlandConfigStructure(t *testing.T) {
|
|||||||
assert.Contains(t, HyprlandConfig, "# KEYBINDINGS")
|
assert.Contains(t, HyprlandConfig, "# KEYBINDINGS")
|
||||||
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec, {{TERMINAL_COMMAND}}")
|
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec, {{TERMINAL_COMMAND}}")
|
||||||
assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle")
|
assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle")
|
||||||
assert.Contains(t, HyprlandConfig, "windowrulev2 = noborder, class:^(com\\.mitchellh\\.ghostty)$")
|
assert.Contains(t, HyprlandConfig, "windowrule = border_size 0, match:class ^(com\\.mitchellh\\.ghostty)$")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGhosttyConfigStructure(t *testing.T) {
|
func TestGhosttyConfigStructure(t *testing.T) {
|
||||||
assert.Contains(t, GhosttyConfig, "window-decoration = false")
|
assert.Contains(t, GhosttyConfig, "window-decoration = false")
|
||||||
assert.Contains(t, GhosttyConfig, "background-opacity = 1.0")
|
assert.Contains(t, GhosttyConfig, "background-opacity = 1.0")
|
||||||
assert.Contains(t, GhosttyConfig, "config-file = ./config-dankcolors")
|
assert.Contains(t, GhosttyConfig, "theme = dankcolors")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGhosttyColorConfigStructure(t *testing.T) {
|
func TestGhosttyColorConfigStructure(t *testing.T) {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func LocateDMSConfig() (string, error) {
|
|||||||
dataDirs = "/usr/local/share:/usr/share"
|
dataDirs = "/usr/local/share:/usr/share"
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dir := range strings.Split(dataDirs, ":") {
|
for dir := range strings.SplitSeq(dataDirs, ":") {
|
||||||
if dir != "" {
|
if dir != "" {
|
||||||
primaryPaths = append(primaryPaths, filepath.Join(dir, "quickshell", "dms"))
|
primaryPaths = append(primaryPaths, filepath.Join(dir, "quickshell", "dms"))
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ func LocateDMSConfig() (string, error) {
|
|||||||
configDirs = "/etc/xdg"
|
configDirs = "/etc/xdg"
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dir := range strings.Split(configDirs, ":") {
|
for dir := range strings.SplitSeq(configDirs, ":") {
|
||||||
if dir != "" {
|
if dir != "" {
|
||||||
primaryPaths = append(primaryPaths, filepath.Join(dir, "quickshell", "dms"))
|
primaryPaths = append(primaryPaths, filepath.Join(dir, "quickshell", "dms"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,4 +48,4 @@ keybind = shift+enter=text:\n
|
|||||||
gtk-single-instance = true
|
gtk-single-instance = true
|
||||||
|
|
||||||
# Dank color generation
|
# Dank color generation
|
||||||
config-file = ./config-dankcolors
|
theme = dankcolors
|
||||||
|
|||||||
@@ -90,36 +90,36 @@ misc {
|
|||||||
# ==================
|
# ==================
|
||||||
# WINDOW RULES
|
# WINDOW RULES
|
||||||
# ==================
|
# ==================
|
||||||
windowrulev2 = tile, class:^(org\.wezfurlong\.wezterm)$
|
windowrule = tile on, match:class ^(org\.wezfurlong\.wezterm)$
|
||||||
|
|
||||||
windowrulev2 = rounding 12, class:^(org\.gnome\.)
|
windowrule = rounding 12, match:class ^(org\.gnome\.)
|
||||||
windowrulev2 = noborder, class:^(org\.gnome\.)
|
windowrule = border_size 0, match:class ^(org\.gnome\.)
|
||||||
|
|
||||||
windowrulev2 = tile, class:^(gnome-control-center)$
|
windowrule = tile on, match:class ^(gnome-control-center)$
|
||||||
windowrulev2 = tile, class:^(pavucontrol)$
|
windowrule = tile on, match:class ^(pavucontrol)$
|
||||||
windowrulev2 = tile, class:^(nm-connection-editor)$
|
windowrule = tile on, match:class ^(nm-connection-editor)$
|
||||||
|
|
||||||
windowrulev2 = float, class:^(gnome-calculator)$
|
windowrule = float on, match:class ^(gnome-calculator)$
|
||||||
windowrulev2 = float, class:^(galculator)$
|
windowrule = float on, match:class ^(galculator)$
|
||||||
windowrulev2 = float, class:^(blueman-manager)$
|
windowrule = float on, match:class ^(blueman-manager)$
|
||||||
windowrulev2 = float, class:^(org\.gnome\.Nautilus)$
|
windowrule = float on, match:class ^(org\.gnome\.Nautilus)$
|
||||||
windowrulev2 = float, class:^(steam)$
|
windowrule = float on, match:class ^(steam)$
|
||||||
windowrulev2 = float, class:^(xdg-desktop-portal)$
|
windowrule = float on, match:class ^(xdg-desktop-portal)$
|
||||||
|
|
||||||
windowrulev2 = noborder, class:^(org\.wezfurlong\.wezterm)$
|
windowrule = border_size 0, match:class ^(org\.wezfurlong\.wezterm)$
|
||||||
windowrulev2 = noborder, class:^(Alacritty)$
|
windowrule = border_size 0, match:class ^(Alacritty)$
|
||||||
windowrulev2 = noborder, class:^(zen)$
|
windowrule = border_size 0, match:class ^(zen)$
|
||||||
windowrulev2 = noborder, class:^(com\.mitchellh\.ghostty)$
|
windowrule = border_size 0, match:class ^(com\.mitchellh\.ghostty)$
|
||||||
windowrulev2 = noborder, class:^(kitty)$
|
windowrule = border_size 0, match:class ^(kitty)$
|
||||||
|
|
||||||
windowrulev2 = float, class:^(firefox)$, title:^(Picture-in-Picture)$
|
windowrule = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture)$
|
||||||
windowrulev2 = float, class:^(zoom)$
|
windowrule = float on, match:class ^(zoom)$
|
||||||
|
|
||||||
# DMS windows floating by default
|
# DMS windows floating by default
|
||||||
windowrulev2 = float, class:^(org.quickshell)$
|
windowrule = float on, match:class ^(org.quickshell)$
|
||||||
windowrulev2 = opacity 0.9 0.9, floating:0, focus:0
|
windowrule = opacity 0.9 0.9, match:float false, match:focus false
|
||||||
|
|
||||||
layerrule = noanim, ^(quickshell)$
|
layerrule = no_anim on, match:namespace ^(quickshell)$
|
||||||
|
|
||||||
# ==================
|
# ==================
|
||||||
# KEYBINDINGS
|
# KEYBINDINGS
|
||||||
@@ -150,6 +150,10 @@ bindel = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3
|
|||||||
bindel = , XF86AudioLowerVolume, exec, dms ipc call audio decrement 3
|
bindel = , XF86AudioLowerVolume, exec, dms ipc call audio decrement 3
|
||||||
bindl = , XF86AudioMute, exec, dms ipc call audio mute
|
bindl = , XF86AudioMute, exec, dms ipc call audio mute
|
||||||
bindl = , XF86AudioMicMute, exec, dms ipc call audio micmute
|
bindl = , XF86AudioMicMute, exec, dms ipc call audio micmute
|
||||||
|
bindl = , XF86AudioPause, exec, dms ipc call mpris playPause
|
||||||
|
bindl = , XF86AudioPlay, exec, dms ipc call mpris playPause
|
||||||
|
bindl = , XF86AudioPrev, exec, dms ipc call mpris previous
|
||||||
|
bindl = , XF86AudioNext, exec, dms ipc call mpris next
|
||||||
|
|
||||||
# === Brightness Controls ===
|
# === Brightness Controls ===
|
||||||
bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""
|
bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""
|
||||||
|
|||||||
@@ -51,6 +51,18 @@ binds {
|
|||||||
XF86AudioMicMute allow-when-locked=true {
|
XF86AudioMicMute allow-when-locked=true {
|
||||||
spawn "dms" "ipc" "call" "audio" "micmute";
|
spawn "dms" "ipc" "call" "audio" "micmute";
|
||||||
}
|
}
|
||||||
|
XF86AudioPause allow-when-locked=true {
|
||||||
|
spawn "dms" "ipc" "call" "mpris" "playPause";
|
||||||
|
}
|
||||||
|
XF86AudioPlay allow-when-locked=true {
|
||||||
|
spawn "dms" "ipc" "call" "mpris" "playPause";
|
||||||
|
}
|
||||||
|
XF86AudioPrev allow-when-locked=true {
|
||||||
|
spawn "dms" "ipc" "call" "mpris" "previous";
|
||||||
|
}
|
||||||
|
XF86AudioNext allow-when-locked=true {
|
||||||
|
spawn "dms" "ipc" "call" "mpris" "next";
|
||||||
|
}
|
||||||
|
|
||||||
// === Brightness Controls ===
|
// === Brightness Controls ===
|
||||||
XF86MonBrightnessUp allow-when-locked=true {
|
XF86MonBrightnessUp allow-when-locked=true {
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode b
|
|||||||
}
|
}
|
||||||
|
|
||||||
step := 0.5
|
step := 0.5
|
||||||
for i := 0; i < 120; i++ {
|
for range 120 {
|
||||||
Lf = math.Max(0, math.Min(100, Lf+dir*step))
|
Lf = math.Max(0, math.Min(100, Lf+dir*step))
|
||||||
cand := labToHex(Lf, af, bf)
|
cand := labToHex(Lf, af, bf)
|
||||||
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
|
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
|
||||||
|
|||||||
@@ -658,7 +658,7 @@ func TestContrastAlgorithmComparison(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
differentCount := 0
|
differentCount := 0
|
||||||
for i := 0; i < 16; i++ {
|
for i := range 16 {
|
||||||
if wcagColors[i].Hex != dpsColors[i].Hex {
|
if wcagColors[i].Hex != dpsColors[i].Hex {
|
||||||
differentCount++
|
differentCount++
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,3 +112,24 @@ func GenerateWeztermTheme(p Palette) string {
|
|||||||
p.Color12.Hex, p.Color13.Hex, p.Color14.Hex, p.Color15.Hex)
|
p.Color12.Hex, p.Color13.Hex, p.Color14.Hex, p.Color15.Hex)
|
||||||
return result.String()
|
return result.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenerateNeovimTheme(p Palette) string {
|
||||||
|
var result strings.Builder
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_0 = \"%s\"\n", p.Color0.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_1 = \"%s\"\n", p.Color1.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_2 = \"%s\"\n", p.Color2.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_3 = \"%s\"\n", p.Color3.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_4 = \"%s\"\n", p.Color4.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_5 = \"%s\"\n", p.Color5.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_6 = \"%s\"\n", p.Color6.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_7 = \"%s\"\n", p.Color7.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_8 = \"%s\"\n", p.Color8.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_9 = \"%s\"\n", p.Color9.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_10 = \"%s\"\n", p.Color10.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_11 = \"%s\"\n", p.Color11.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_12 = \"%s\"\n", p.Color12.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_13 = \"%s\"\n", p.Color13.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_14 = \"%s\"\n", p.Color14.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_15 = \"%s\"\n", p.Color15.Hex)
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
@@ -514,12 +515,9 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
|
|||||||
dmsShell = append(dmsShell, pkg)
|
dmsShell = append(dmsShell, pkg)
|
||||||
} else {
|
} else {
|
||||||
isDep := false
|
isDep := false
|
||||||
for _, dep := range dmsDepencies {
|
if slices.Contains(dmsDepencies, pkg) {
|
||||||
if pkg == dep {
|
deps = append(deps, pkg)
|
||||||
deps = append(deps, pkg)
|
isDep = true
|
||||||
isDep = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !isDep {
|
if !isDep {
|
||||||
others = append(others, pkg)
|
others = append(others, pkg)
|
||||||
@@ -545,7 +543,7 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
a.log(fmt.Sprintf("Warning: failed to clean existing cache for %s: %v", pkg, err))
|
a.log(fmt.Sprintf("Warning: failed to clean existing cache for %s: %v", pkg, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
if err := os.MkdirAll(buildDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create build directory: %w", err)
|
return fmt.Errorf("failed to create build directory: %w", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|||||||
@@ -550,10 +550,7 @@ func (b *BaseDistribution) WriteEnvironmentConfig(terminal deps.Terminal) error
|
|||||||
terminalCmd = "ghostty"
|
terminalCmd = "ghostty"
|
||||||
}
|
}
|
||||||
|
|
||||||
content := fmt.Sprintf(`QT_QPA_PLATFORM=wayland
|
content := fmt.Sprintf(`ELECTRON_OZONE_PLATFORM_HINT=auto
|
||||||
ELECTRON_OZONE_PLATFORM_HINT=auto
|
|
||||||
QT_QPA_PLATFORMTHEME=gtk3
|
|
||||||
QT_QPA_PLATFORMTHEME_QT6=gtk3
|
|
||||||
TERMINAL=%s
|
TERMINAL=%s
|
||||||
`, terminalCmd)
|
`, terminalCmd)
|
||||||
|
|
||||||
@@ -567,12 +564,6 @@ TERMINAL=%s
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *BaseDistribution) EnableDMSService(ctx context.Context, wm deps.WindowManager) error {
|
func (b *BaseDistribution) EnableDMSService(ctx context.Context, wm deps.WindowManager) error {
|
||||||
cmd := exec.CommandContext(ctx, "systemctl", "--user", "enable", "--now", "dms")
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to enable dms service: %w", err)
|
|
||||||
}
|
|
||||||
b.log("Enabled dms systemd user service")
|
|
||||||
|
|
||||||
switch wm {
|
switch wm {
|
||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "niri.service", "dms").Run(); err != nil {
|
if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "niri.service", "dms").Run(); err != nil {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
@@ -384,6 +385,8 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
debianVersion := "Debian_13"
|
debianVersion := "Debian_13"
|
||||||
if osInfo.VersionID == "testing" {
|
if osInfo.VersionID == "testing" {
|
||||||
debianVersion = "Debian_Testing"
|
debianVersion = "Debian_Testing"
|
||||||
|
} else if osInfo.VersionCodename == "sid" || osInfo.VersionID == "sid" || strings.Contains(strings.ToLower(osInfo.PrettyName), "sid") || strings.Contains(strings.ToLower(osInfo.PrettyName), "unstable") {
|
||||||
|
debianVersion = "Debian_Unstable"
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, pkg := range obsPkgs {
|
for _, pkg := range obsPkgs {
|
||||||
@@ -427,7 +430,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add repository
|
// Add repository
|
||||||
repoLine := fmt.Sprintf("deb [signed-by=%s] %s/ /", keyringPath, baseURL)
|
repoLine := fmt.Sprintf("deb [signed-by=%s, arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL)
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: PhaseSystemPackages,
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ type ManualPackageInstaller struct {
|
|||||||
|
|
||||||
// parseLatestTagFromGitOutput parses git ls-remote output and returns the latest tag
|
// parseLatestTagFromGitOutput parses git ls-remote output and returns the latest tag
|
||||||
func (m *ManualPackageInstaller) parseLatestTagFromGitOutput(output string) string {
|
func (m *ManualPackageInstaller) parseLatestTagFromGitOutput(output string) string {
|
||||||
lines := strings.Split(output, "\n")
|
lines := strings.SplitSeq(output, "\n")
|
||||||
for _, line := range lines {
|
for line := range lines {
|
||||||
if strings.Contains(line, "refs/tags/") && !strings.Contains(line, "^{}") {
|
if strings.Contains(line, "refs/tags/") && !strings.Contains(line, "^{}") {
|
||||||
parts := strings.Split(line, "refs/tags/")
|
parts := strings.Split(line, "refs/tags/")
|
||||||
if len(parts) > 1 {
|
if len(parts) > 1 {
|
||||||
@@ -103,12 +103,12 @@ func (m *ManualPackageInstaller) installDgop(ctx context.Context, sudoPassword s
|
|||||||
}
|
}
|
||||||
|
|
||||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpDir := filepath.Join(cacheDir, "dgop-build")
|
tmpDir := filepath.Join(cacheDir, "dgop-build")
|
||||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
@@ -160,10 +160,10 @@ func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword s
|
|||||||
homeDir, _ := os.UserHomeDir()
|
homeDir, _ := os.UserHomeDir()
|
||||||
buildDir := filepath.Join(homeDir, ".cache", "dankinstall", "niri-build")
|
buildDir := filepath.Join(homeDir, ".cache", "dankinstall", "niri-build")
|
||||||
tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "tmp")
|
tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "tmp")
|
||||||
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
if err := os.MkdirAll(buildDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create build directory: %w", err)
|
return fmt.Errorf("failed to create build directory: %w", err)
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -237,12 +237,12 @@ func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, variant
|
|||||||
}
|
}
|
||||||
|
|
||||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpDir := filepath.Join(cacheDir, "quickshell-build")
|
tmpDir := filepath.Join(cacheDir, "quickshell-build")
|
||||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
@@ -273,7 +273,7 @@ func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, variant
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildDir := tmpDir + "/build"
|
buildDir := tmpDir + "/build"
|
||||||
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
if err := os.MkdirAll(buildDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create build directory: %w", err)
|
return fmt.Errorf("failed to create build directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,12 +343,12 @@ func (m *ManualPackageInstaller) installHyprland(ctx context.Context, sudoPasswo
|
|||||||
}
|
}
|
||||||
|
|
||||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpDir := filepath.Join(cacheDir, "hyprland-build")
|
tmpDir := filepath.Join(cacheDir, "hyprland-build")
|
||||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
@@ -406,12 +406,12 @@ func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPasswor
|
|||||||
}
|
}
|
||||||
|
|
||||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpDir := filepath.Join(cacheDir, "ghostty-build")
|
tmpDir := filepath.Join(cacheDir, "ghostty-build")
|
||||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
@@ -528,7 +528,7 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
|
|||||||
}
|
}
|
||||||
|
|
||||||
configDir := filepath.Dir(dmsPath)
|
configDir := filepath.Dir(dmsPath)
|
||||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create quickshell config directory: %w", err)
|
return fmt.Errorf("failed to create quickshell config directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ func init() {
|
|||||||
Register("opensuse-tumbleweed", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution {
|
Register("opensuse-tumbleweed", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
return NewOpenSUSEDistribution(config, logChan)
|
return NewOpenSUSEDistribution(config, logChan)
|
||||||
})
|
})
|
||||||
|
Register("opensuse-leap", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewOpenSUSEDistribution(config, logChan)
|
||||||
|
})
|
||||||
|
Register("opensuse-slowroll", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewOpenSUSEDistribution(config, logChan)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpenSUSEDistribution struct {
|
type OpenSUSEDistribution struct {
|
||||||
@@ -434,6 +440,19 @@ func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []
|
|||||||
func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
enabledRepos := make(map[string]bool)
|
enabledRepos := make(map[string]bool)
|
||||||
|
|
||||||
|
osInfo, err := GetOSInfo()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get OS info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
obsDistroVersion := "openSUSE_Tumbleweed"
|
||||||
|
switch osInfo.Distribution.ID {
|
||||||
|
case "opensuse-leap":
|
||||||
|
obsDistroVersion = fmt.Sprintf("openSUSE_Leap_%s", osInfo.VersionID)
|
||||||
|
case "opensuse-slowroll":
|
||||||
|
obsDistroVersion = "openSUSE_Slowroll"
|
||||||
|
}
|
||||||
|
|
||||||
for _, pkg := range obsPkgs {
|
for _, pkg := range obsPkgs {
|
||||||
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
|
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
|
||||||
o.log(fmt.Sprintf("Enabling OBS repository: %s", pkg.RepoURL))
|
o.log(fmt.Sprintf("Enabling OBS repository: %s", pkg.RepoURL))
|
||||||
@@ -441,8 +460,8 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
|||||||
// RepoURL format: "home:AvengeMedia:danklinux"
|
// RepoURL format: "home:AvengeMedia:danklinux"
|
||||||
repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/")
|
repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/")
|
||||||
repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-")
|
repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-")
|
||||||
repoURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/openSUSE_Tumbleweed/%s.repo",
|
repoURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/%s/%s.repo",
|
||||||
repoPath, pkg.RepoURL)
|
repoPath, obsDistroVersion, pkg.RepoURL)
|
||||||
|
|
||||||
checkCmd := exec.CommandContext(ctx, "zypper", "repos", repoName)
|
checkCmd := exec.CommandContext(ctx, "zypper", "repos", repoName)
|
||||||
if checkCmd.Run() == nil {
|
if checkCmd.Run() == nil {
|
||||||
|
|||||||
@@ -19,11 +19,12 @@ type DistroInfo struct {
|
|||||||
|
|
||||||
// OSInfo contains complete OS information
|
// OSInfo contains complete OS information
|
||||||
type OSInfo struct {
|
type OSInfo struct {
|
||||||
Distribution DistroInfo
|
Distribution DistroInfo
|
||||||
Version string
|
Version string
|
||||||
VersionID string
|
VersionID string
|
||||||
PrettyName string
|
VersionCodename string
|
||||||
Architecture string
|
PrettyName string
|
||||||
|
Architecture string
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOSInfo detects the current OS and returns information about it
|
// GetOSInfo detects the current OS and returns information about it
|
||||||
@@ -72,6 +73,8 @@ func GetOSInfo() (*OSInfo, error) {
|
|||||||
info.VersionID = value
|
info.VersionID = value
|
||||||
case "VERSION":
|
case "VERSION":
|
||||||
info.Version = value
|
info.Version = value
|
||||||
|
case "VERSION_CODENAME":
|
||||||
|
info.VersionCodename = value
|
||||||
case "PRETTY_NAME":
|
case "PRETTY_NAME":
|
||||||
info.PrettyName = value
|
info.PrettyName = value
|
||||||
}
|
}
|
||||||
@@ -100,6 +103,10 @@ func IsUnsupportedDistro(distroID, versionID string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if distroID == "debian" {
|
if distroID == "debian" {
|
||||||
|
// unstable/sid support
|
||||||
|
if versionID == "sid" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if versionID == "" {
|
if versionID == "" {
|
||||||
// debian testing/sid have no version ID
|
// debian testing/sid have no version ID
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func DefaultDiscoveryConfig() *DiscoveryConfig {
|
|||||||
|
|
||||||
configDirs := os.Getenv("XDG_CONFIG_DIRS")
|
configDirs := os.Getenv("XDG_CONFIG_DIRS")
|
||||||
if configDirs != "" {
|
if configDirs != "" {
|
||||||
for _, dir := range strings.Split(configDirs, ":") {
|
for dir := range strings.SplitSeq(configDirs, ":") {
|
||||||
if dir != "" {
|
if dir != "" {
|
||||||
searchPaths = append(searchPaths, filepath.Join(dir, "DankMaterialShell", "cheatsheets"))
|
searchPaths = append(searchPaths, filepath.Join(dir, "DankMaterialShell", "cheatsheets"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ func TestNewJSONFileProvider(t *testing.T) {
|
|||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
testFile := filepath.Join(tmpDir, "test.json")
|
testFile := filepath.Join(tmpDir, "test.json")
|
||||||
|
|
||||||
if err := os.WriteFile(testFile, []byte("{}"), 0644); err != nil {
|
if err := os.WriteFile(testFile, []byte("{}"), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to create test file: %v", err)
|
t.Fatalf("Failed to create test file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ func TestJSONFileProviderGetCheatSheet(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
if err := os.WriteFile(testFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(testFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test file: %v", err)
|
t.Fatalf("Failed to write test file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ func TestJSONFileProviderGetCheatSheetNoProvider(t *testing.T) {
|
|||||||
"binds": {}
|
"binds": {}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
if err := os.WriteFile(testFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(testFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test file: %v", err)
|
t.Fatalf("Failed to write test file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +181,7 @@ func TestJSONFileProviderFlatArrayBackwardsCompat(t *testing.T) {
|
|||||||
]
|
]
|
||||||
}`
|
}`
|
||||||
|
|
||||||
if err := os.WriteFile(testFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(testFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test file: %v", err)
|
t.Fatalf("Failed to write test file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +216,7 @@ func TestJSONFileProviderInvalidJSON(t *testing.T) {
|
|||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
testFile := filepath.Join(tmpDir, "invalid.json")
|
testFile := filepath.Join(tmpDir, "invalid.json")
|
||||||
|
|
||||||
if err := os.WriteFile(testFile, []byte("not valid json"), 0644); err != nil {
|
if err := os.WriteFile(testFile, []byte("not valid json"), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test file: %v", err)
|
t.Fatalf("Failed to write test file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,22 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ColorMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ColorModeDark ColorMode = "dark"
|
||||||
|
ColorModeLight ColorMode = "light"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *ColorMode) GTKTheme() string {
|
||||||
|
switch *c {
|
||||||
|
case ColorModeDark:
|
||||||
|
return "adw-gtk3-dark"
|
||||||
|
default:
|
||||||
|
return "adw-gtk3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
matugenVersionOnce sync.Once
|
matugenVersionOnce sync.Once
|
||||||
matugenSupportsCOE bool
|
matugenSupportsCOE bool
|
||||||
@@ -27,7 +43,7 @@ type Options struct {
|
|||||||
ConfigDir string
|
ConfigDir string
|
||||||
Kind string
|
Kind string
|
||||||
Value string
|
Value string
|
||||||
Mode string
|
Mode ColorMode
|
||||||
IconTheme string
|
IconTheme string
|
||||||
MatugenType string
|
MatugenType string
|
||||||
RunUserTemplates bool
|
RunUserTemplates bool
|
||||||
@@ -35,6 +51,7 @@ type Options struct {
|
|||||||
SyncModeWithPortal bool
|
SyncModeWithPortal bool
|
||||||
TerminalsAlwaysDark bool
|
TerminalsAlwaysDark bool
|
||||||
SkipTemplates string
|
SkipTemplates string
|
||||||
|
AppChecker utils.AppChecker
|
||||||
}
|
}
|
||||||
|
|
||||||
type ColorsOutput struct {
|
type ColorsOutput struct {
|
||||||
@@ -77,7 +94,7 @@ func Run(opts Options) error {
|
|||||||
return fmt.Errorf("value is required")
|
return fmt.Errorf("value is required")
|
||||||
}
|
}
|
||||||
if opts.Mode == "" {
|
if opts.Mode == "" {
|
||||||
opts.Mode = "dark"
|
opts.Mode = ColorModeDark
|
||||||
}
|
}
|
||||||
if opts.MatugenType == "" {
|
if opts.MatugenType == "" {
|
||||||
opts.MatugenType = "scheme-tonal-spot"
|
opts.MatugenType = "scheme-tonal-spot"
|
||||||
@@ -85,6 +102,9 @@ func Run(opts Options) error {
|
|||||||
if opts.IconTheme == "" {
|
if opts.IconTheme == "" {
|
||||||
opts.IconTheme = "System Default"
|
opts.IconTheme = "System Default"
|
||||||
}
|
}
|
||||||
|
if opts.AppChecker == nil {
|
||||||
|
opts.AppChecker = utils.DefaultAppChecker{}
|
||||||
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(opts.StateDir, 0755); err != nil {
|
if err := os.MkdirAll(opts.StateDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create state dir: %w", err)
|
return fmt.Errorf("failed to create state dir: %w", err)
|
||||||
@@ -145,7 +165,7 @@ func buildOnce(opts *Options) error {
|
|||||||
importArgs = []string{"--import-json-string", importData}
|
importArgs = []string{"--import-json-string", importData}
|
||||||
|
|
||||||
log.Info("Running matugen color hex with stock color overrides")
|
log.Info("Running matugen color hex with stock color overrides")
|
||||||
args := []string{"color", "hex", primaryDark, "-m", 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 err
|
||||||
@@ -181,7 +201,7 @@ func buildOnce(opts *Options) error {
|
|||||||
default:
|
default:
|
||||||
args = []string{opts.Kind, opts.Value}
|
args = []string{opts.Kind, opts.Value}
|
||||||
}
|
}
|
||||||
args = append(args, "-m", 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 err
|
||||||
@@ -234,62 +254,70 @@ output_path = '%s'
|
|||||||
if !opts.ShouldSkipTemplate("gtk") {
|
if !opts.ShouldSkipTemplate("gtk") {
|
||||||
switch opts.Mode {
|
switch opts.Mode {
|
||||||
case "light":
|
case "light":
|
||||||
appendConfig(opts, cfgFile, "skip", "gtk3-light.toml")
|
appendConfig(opts, cfgFile, nil, nil, "gtk3-light.toml")
|
||||||
default:
|
default:
|
||||||
appendConfig(opts, cfgFile, "skip", "gtk3-dark.toml")
|
appendConfig(opts, cfgFile, nil, nil, "gtk3-dark.toml")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !opts.ShouldSkipTemplate("niri") {
|
if !opts.ShouldSkipTemplate("niri") {
|
||||||
appendConfig(opts, cfgFile, "niri", "niri.toml")
|
appendConfig(opts, cfgFile, []string{"niri"}, nil, "niri.toml")
|
||||||
}
|
}
|
||||||
if !opts.ShouldSkipTemplate("qt5ct") {
|
if !opts.ShouldSkipTemplate("qt5ct") {
|
||||||
appendConfig(opts, cfgFile, "qt5ct", "qt5ct.toml")
|
appendConfig(opts, cfgFile, []string{"qt5ct"}, nil, "qt5ct.toml")
|
||||||
}
|
}
|
||||||
if !opts.ShouldSkipTemplate("qt6ct") {
|
if !opts.ShouldSkipTemplate("qt6ct") {
|
||||||
appendConfig(opts, cfgFile, "qt6ct", "qt6ct.toml")
|
appendConfig(opts, cfgFile, []string{"qt6ct"}, nil, "qt6ct.toml")
|
||||||
}
|
}
|
||||||
if !opts.ShouldSkipTemplate("firefox") {
|
if !opts.ShouldSkipTemplate("firefox") {
|
||||||
appendConfig(opts, cfgFile, "firefox", "firefox.toml")
|
appendConfig(opts, cfgFile, []string{"firefox"}, nil, "firefox.toml")
|
||||||
}
|
}
|
||||||
if !opts.ShouldSkipTemplate("pywalfox") {
|
if !opts.ShouldSkipTemplate("pywalfox") {
|
||||||
appendConfig(opts, cfgFile, "pywalfox", "pywalfox.toml")
|
appendConfig(opts, cfgFile, []string{"pywalfox"}, nil, "pywalfox.toml")
|
||||||
|
}
|
||||||
|
if !opts.ShouldSkipTemplate("zenbrowser") {
|
||||||
|
appendConfig(opts, cfgFile, []string{"zen", "zen-browser"}, []string{"app.zen_browser.zen"}, "zenbrowser.toml")
|
||||||
}
|
}
|
||||||
if !opts.ShouldSkipTemplate("vesktop") {
|
if !opts.ShouldSkipTemplate("vesktop") {
|
||||||
appendConfig(opts, cfgFile, "vesktop", "vesktop.toml")
|
appendConfig(opts, cfgFile, []string{"vesktop"}, []string{"dev.vencord.Vesktop"}, "vesktop.toml")
|
||||||
|
}
|
||||||
|
if !opts.ShouldSkipTemplate("equibop") {
|
||||||
|
appendConfig(opts, cfgFile, []string{"equibop"}, nil, "equibop.toml")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !opts.ShouldSkipTemplate("ghostty") {
|
if !opts.ShouldSkipTemplate("ghostty") {
|
||||||
appendTerminalConfig(opts, cfgFile, tmpDir, "ghostty", "ghostty.toml")
|
appendTerminalConfig(opts, cfgFile, tmpDir, []string{"ghostty"}, nil, "ghostty.toml")
|
||||||
}
|
}
|
||||||
if !opts.ShouldSkipTemplate("kitty") {
|
if !opts.ShouldSkipTemplate("kitty") {
|
||||||
appendTerminalConfig(opts, cfgFile, tmpDir, "kitty", "kitty.toml")
|
appendTerminalConfig(opts, cfgFile, tmpDir, []string{"kitty"}, nil, "kitty.toml")
|
||||||
}
|
}
|
||||||
if !opts.ShouldSkipTemplate("foot") {
|
if !opts.ShouldSkipTemplate("foot") {
|
||||||
appendTerminalConfig(opts, cfgFile, tmpDir, "foot", "foot.toml")
|
appendTerminalConfig(opts, cfgFile, tmpDir, []string{"foot"}, nil, "foot.toml")
|
||||||
}
|
}
|
||||||
if !opts.ShouldSkipTemplate("alacritty") {
|
if !opts.ShouldSkipTemplate("alacritty") {
|
||||||
appendTerminalConfig(opts, cfgFile, tmpDir, "alacritty", "alacritty.toml")
|
appendTerminalConfig(opts, cfgFile, tmpDir, []string{"alacritty"}, nil, "alacritty.toml")
|
||||||
}
|
}
|
||||||
if !opts.ShouldSkipTemplate("wezterm") {
|
if !opts.ShouldSkipTemplate("wezterm") {
|
||||||
appendTerminalConfig(opts, cfgFile, tmpDir, "wezterm", "wezterm.toml")
|
appendTerminalConfig(opts, cfgFile, tmpDir, []string{"wezterm"}, nil, "wezterm.toml")
|
||||||
|
}
|
||||||
|
if !opts.ShouldSkipTemplate("nvim") {
|
||||||
|
appendTerminalConfig(opts, cfgFile, tmpDir, []string{"nvim"}, nil, "neovim.toml")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !opts.ShouldSkipTemplate("dgop") {
|
if !opts.ShouldSkipTemplate("dgop") {
|
||||||
appendConfig(opts, cfgFile, "dgop", "dgop.toml")
|
appendConfig(opts, cfgFile, []string{"dgop"}, nil, "dgop.toml")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !opts.ShouldSkipTemplate("kcolorscheme") {
|
if !opts.ShouldSkipTemplate("kcolorscheme") {
|
||||||
appendConfig(opts, cfgFile, "skip", "kcolorscheme.toml")
|
appendConfig(opts, cfgFile, nil, nil, "kcolorscheme.toml")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !opts.ShouldSkipTemplate("vscode") {
|
if !opts.ShouldSkipTemplate("vscode") {
|
||||||
homeDir, _ := os.UserHomeDir()
|
homeDir, _ := os.UserHomeDir()
|
||||||
appendVSCodeConfig(cfgFile, "vscode", filepath.Join(homeDir, ".vscode/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
appendVSCodeConfig(cfgFile, "vscode", filepath.Join(homeDir, ".vscode/extensions"), opts.ShellDir)
|
||||||
appendVSCodeConfig(cfgFile, "codium", filepath.Join(homeDir, ".vscode-oss/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
appendVSCodeConfig(cfgFile, "codium", filepath.Join(homeDir, ".vscode-oss/extensions"), opts.ShellDir)
|
||||||
appendVSCodeConfig(cfgFile, "codeoss", filepath.Join(homeDir, ".config/Code - OSS/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
appendVSCodeConfig(cfgFile, "codeoss", filepath.Join(homeDir, ".config/Code - OSS/extensions"), opts.ShellDir)
|
||||||
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir)
|
||||||
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions"), opts.ShellDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.RunUserTemplates {
|
if opts.RunUserTemplates {
|
||||||
@@ -318,12 +346,18 @@ output_path = '%s'
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendConfig(opts *Options, cfgFile *os.File, checkCmd, fileName string) {
|
func appendConfig(
|
||||||
|
opts *Options,
|
||||||
|
cfgFile *os.File,
|
||||||
|
checkCmd []string,
|
||||||
|
checkFlatpaks []string,
|
||||||
|
fileName string,
|
||||||
|
) {
|
||||||
configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName)
|
configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName)
|
||||||
if _, err := os.Stat(configPath); err != nil {
|
if _, err := os.Stat(configPath); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if checkCmd != "skip" && !utils.CommandExists(checkCmd) {
|
if !appExists(opts.AppChecker, checkCmd, checkFlatpaks) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data, err := os.ReadFile(configPath)
|
data, err := os.ReadFile(configPath)
|
||||||
@@ -334,12 +368,12 @@ func appendConfig(opts *Options, cfgFile *os.File, checkCmd, fileName string) {
|
|||||||
cfgFile.WriteString("\n")
|
cfgFile.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendTerminalConfig(opts *Options, cfgFile *os.File, tmpDir, checkCmd, fileName string) {
|
func appendTerminalConfig(opts *Options, cfgFile *os.File, tmpDir string, checkCmd []string, checkFlatpaks []string, fileName string) {
|
||||||
configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName)
|
configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName)
|
||||||
if _, err := os.Stat(configPath); err != nil {
|
if _, err := os.Stat(configPath); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if checkCmd != "skip" && !utils.CommandExists(checkCmd) {
|
if !appExists(opts.AppChecker, checkCmd, checkFlatpaks) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data, err := os.ReadFile(configPath)
|
data, err := os.ReadFile(configPath)
|
||||||
@@ -392,10 +426,28 @@ func appendTerminalConfig(opts *Options, cfgFile *os.File, tmpDir, checkCmd, fil
|
|||||||
cfgFile.WriteString("\n")
|
cfgFile.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendVSCodeConfig(cfgFile *os.File, name, extDir, shellDir string) {
|
func appExists(checker utils.AppChecker, checkCmd []string, checkFlatpaks []string) bool {
|
||||||
if _, err := os.Stat(extDir); err != nil {
|
// Both nil is treated as "skip check" / unconditionally run
|
||||||
|
if checkCmd == nil && checkFlatpaks == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if checkCmd != nil && checker.AnyCommandExists(checkCmd...) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if checkFlatpaks != nil && checker.AnyFlatpakExists(checkFlatpaks...) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendVSCodeConfig(cfgFile *os.File, name, extBaseDir, shellDir string) {
|
||||||
|
pattern := filepath.Join(extBaseDir, "danklinux.dms-theme-*")
|
||||||
|
matches, err := filepath.Glob(pattern)
|
||||||
|
if err != nil || len(matches) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extDir := matches[0]
|
||||||
templateDir := filepath.Join(shellDir, "matugen", "templates")
|
templateDir := filepath.Join(shellDir, "matugen", "templates")
|
||||||
fmt.Fprintf(cfgFile, `[templates.dms%sdefault]
|
fmt.Fprintf(cfgFile, `[templates.dms%sdefault]
|
||||||
input_path = '%s/vscode-color-theme-default.json'
|
input_path = '%s/vscode-color-theme-default.json'
|
||||||
@@ -548,19 +600,19 @@ func extractNestedColor(jsonStr, colorName, variant string) string {
|
|||||||
return color
|
return color
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateDank16Variants(primaryDark, primaryLight, surface, mode string) string {
|
func generateDank16Variants(primaryDark, primaryLight, surface string, mode ColorMode) string {
|
||||||
variantOpts := dank16.VariantOptions{
|
variantOpts := dank16.VariantOptions{
|
||||||
PrimaryDark: primaryDark,
|
PrimaryDark: primaryDark,
|
||||||
PrimaryLight: primaryLight,
|
PrimaryLight: primaryLight,
|
||||||
Background: surface,
|
Background: surface,
|
||||||
UseDPS: true,
|
UseDPS: true,
|
||||||
IsLightMode: mode == "light",
|
IsLightMode: mode == ColorModeLight,
|
||||||
}
|
}
|
||||||
variantColors := dank16.GenerateVariantPalette(variantOpts)
|
variantColors := dank16.GenerateVariantPalette(variantOpts)
|
||||||
return dank16.GenerateVariantJSON(variantColors)
|
return dank16.GenerateVariantJSON(variantColors)
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshGTK(configDir, mode string) {
|
func refreshGTK(configDir string, mode ColorMode) {
|
||||||
gtkCSS := filepath.Join(configDir, "gtk-3.0", "gtk.css")
|
gtkCSS := filepath.Join(configDir, "gtk-3.0", "gtk.css")
|
||||||
|
|
||||||
info, err := os.Lstat(gtkCSS)
|
info, err := os.Lstat(gtkCSS)
|
||||||
@@ -586,7 +638,7 @@ func refreshGTK(configDir, mode string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", "").Run()
|
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", "").Run()
|
||||||
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", "adw-gtk3-"+mode).Run()
|
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", mode.GTKTheme()).Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func signalTerminals() {
|
func signalTerminals() {
|
||||||
@@ -616,9 +668,9 @@ func signalByName(name string, sig syscall.Signal) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncColorScheme(mode string) {
|
func syncColorScheme(mode ColorMode) {
|
||||||
scheme := "prefer-dark"
|
scheme := "prefer-dark"
|
||||||
if mode == "light" {
|
if mode == ColorModeLight {
|
||||||
scheme = "default"
|
scheme = "default"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
323
core/internal/matugen/matugen_test.go
Normal file
323
core/internal/matugen/matugen_test.go
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
package matugen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAppendConfigBinaryExists(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
|
if err := os.MkdirAll(configsDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testConfig := "test config content"
|
||||||
|
configPath := filepath.Join(configsDir, "test.toml")
|
||||||
|
if err := os.WriteFile(configPath, []byte(testConfig), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to write config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outFile := filepath.Join(tempDir, "output.toml")
|
||||||
|
cfgFile, err := os.Create(outFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create output file: %v", err)
|
||||||
|
}
|
||||||
|
defer cfgFile.Close()
|
||||||
|
|
||||||
|
mockChecker := mocks_utils.NewMockAppChecker(t)
|
||||||
|
mockChecker.EXPECT().AnyCommandExists("sh").Return(true)
|
||||||
|
|
||||||
|
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
||||||
|
|
||||||
|
appendConfig(opts, cfgFile, []string{"sh"}, nil, "test.toml")
|
||||||
|
|
||||||
|
cfgFile.Close()
|
||||||
|
output, err := os.ReadFile(outFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read output: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(output) == 0 {
|
||||||
|
t.Errorf("expected config to be written when binary exists")
|
||||||
|
}
|
||||||
|
if string(output) != testConfig+"\n" {
|
||||||
|
t.Errorf("expected %q, got %q", testConfig+"\n", string(output))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppendConfigBinaryDoesNotExist(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
|
if err := os.MkdirAll(configsDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testConfig := "test config content"
|
||||||
|
configPath := filepath.Join(configsDir, "test.toml")
|
||||||
|
if err := os.WriteFile(configPath, []byte(testConfig), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to write config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outFile := filepath.Join(tempDir, "output.toml")
|
||||||
|
cfgFile, err := os.Create(outFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create output file: %v", err)
|
||||||
|
}
|
||||||
|
defer cfgFile.Close()
|
||||||
|
|
||||||
|
mockChecker := mocks_utils.NewMockAppChecker(t)
|
||||||
|
mockChecker.EXPECT().AnyCommandExists("nonexistent-binary-12345").Return(false)
|
||||||
|
mockChecker.EXPECT().AnyFlatpakExists().Return(false)
|
||||||
|
|
||||||
|
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
||||||
|
|
||||||
|
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{}, "test.toml")
|
||||||
|
|
||||||
|
cfgFile.Close()
|
||||||
|
output, err := os.ReadFile(outFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read output: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(output) != 0 {
|
||||||
|
t.Errorf("expected no config when binary doesn't exist, got: %q", string(output))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppendConfigFlatpakExists(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
|
if err := os.MkdirAll(configsDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testConfig := "zen config content"
|
||||||
|
configPath := filepath.Join(configsDir, "test.toml")
|
||||||
|
if err := os.WriteFile(configPath, []byte(testConfig), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to write config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outFile := filepath.Join(tempDir, "output.toml")
|
||||||
|
cfgFile, err := os.Create(outFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create output file: %v", err)
|
||||||
|
}
|
||||||
|
defer cfgFile.Close()
|
||||||
|
|
||||||
|
mockChecker := mocks_utils.NewMockAppChecker(t)
|
||||||
|
mockChecker.EXPECT().AnyFlatpakExists("app.zen_browser.zen").Return(true)
|
||||||
|
|
||||||
|
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
||||||
|
|
||||||
|
appendConfig(opts, cfgFile, nil, []string{"app.zen_browser.zen"}, "test.toml")
|
||||||
|
|
||||||
|
cfgFile.Close()
|
||||||
|
output, err := os.ReadFile(outFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read output: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(output) == 0 {
|
||||||
|
t.Errorf("expected config to be written when flatpak exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppendConfigFlatpakDoesNotExist(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
|
if err := os.MkdirAll(configsDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testConfig := "test config content"
|
||||||
|
configPath := filepath.Join(configsDir, "test.toml")
|
||||||
|
if err := os.WriteFile(configPath, []byte(testConfig), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to write config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outFile := filepath.Join(tempDir, "output.toml")
|
||||||
|
cfgFile, err := os.Create(outFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create output file: %v", err)
|
||||||
|
}
|
||||||
|
defer cfgFile.Close()
|
||||||
|
|
||||||
|
mockChecker := mocks_utils.NewMockAppChecker(t)
|
||||||
|
mockChecker.EXPECT().AnyCommandExists().Return(false)
|
||||||
|
mockChecker.EXPECT().AnyFlatpakExists("com.nonexistent.flatpak").Return(false)
|
||||||
|
|
||||||
|
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
||||||
|
|
||||||
|
appendConfig(opts, cfgFile, []string{}, []string{"com.nonexistent.flatpak"}, "test.toml")
|
||||||
|
|
||||||
|
cfgFile.Close()
|
||||||
|
output, err := os.ReadFile(outFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read output: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(output) != 0 {
|
||||||
|
t.Errorf("expected no config when flatpak doesn't exist, got: %q", string(output))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppendConfigBothExist(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
|
if err := os.MkdirAll(configsDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testConfig := "zen config content"
|
||||||
|
configPath := filepath.Join(configsDir, "test.toml")
|
||||||
|
if err := os.WriteFile(configPath, []byte(testConfig), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to write config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outFile := filepath.Join(tempDir, "output.toml")
|
||||||
|
cfgFile, err := os.Create(outFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create output file: %v", err)
|
||||||
|
}
|
||||||
|
defer cfgFile.Close()
|
||||||
|
|
||||||
|
mockChecker := mocks_utils.NewMockAppChecker(t)
|
||||||
|
mockChecker.EXPECT().AnyCommandExists("sh").Return(true)
|
||||||
|
|
||||||
|
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
||||||
|
|
||||||
|
appendConfig(opts, cfgFile, []string{"sh"}, []string{"app.zen_browser.zen"}, "test.toml")
|
||||||
|
|
||||||
|
cfgFile.Close()
|
||||||
|
output, err := os.ReadFile(outFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read output: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(output) == 0 {
|
||||||
|
t.Errorf("expected config to be written when both binary and flatpak exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppendConfigNeitherExists(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
|
if err := os.MkdirAll(configsDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testConfig := "test config content"
|
||||||
|
configPath := filepath.Join(configsDir, "test.toml")
|
||||||
|
if err := os.WriteFile(configPath, []byte(testConfig), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to write config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outFile := filepath.Join(tempDir, "output.toml")
|
||||||
|
cfgFile, err := os.Create(outFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create output file: %v", err)
|
||||||
|
}
|
||||||
|
defer cfgFile.Close()
|
||||||
|
|
||||||
|
mockChecker := mocks_utils.NewMockAppChecker(t)
|
||||||
|
mockChecker.EXPECT().AnyCommandExists("nonexistent-binary-12345").Return(false)
|
||||||
|
mockChecker.EXPECT().AnyFlatpakExists("com.nonexistent.flatpak").Return(false)
|
||||||
|
|
||||||
|
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
||||||
|
|
||||||
|
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{"com.nonexistent.flatpak"}, "test.toml")
|
||||||
|
|
||||||
|
cfgFile.Close()
|
||||||
|
output, err := os.ReadFile(outFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read output: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(output) != 0 {
|
||||||
|
t.Errorf("expected no config when neither exists, got: %q", string(output))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppendConfigNoChecks(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
|
if err := os.MkdirAll(configsDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testConfig := "always include"
|
||||||
|
configPath := filepath.Join(configsDir, "test.toml")
|
||||||
|
if err := os.WriteFile(configPath, []byte(testConfig), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to write config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outFile := filepath.Join(tempDir, "output.toml")
|
||||||
|
cfgFile, err := os.Create(outFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create output file: %v", err)
|
||||||
|
}
|
||||||
|
defer cfgFile.Close()
|
||||||
|
|
||||||
|
opts := &Options{ShellDir: shellDir}
|
||||||
|
|
||||||
|
appendConfig(opts, cfgFile, nil, nil, "test.toml")
|
||||||
|
|
||||||
|
cfgFile.Close()
|
||||||
|
output, err := os.ReadFile(outFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read output: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(output) == 0 {
|
||||||
|
t.Errorf("expected config to be written when no checks specified")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppendConfigFileDoesNotExist(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
|
if err := os.MkdirAll(configsDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outFile := filepath.Join(tempDir, "output.toml")
|
||||||
|
cfgFile, err := os.Create(outFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create output file: %v", err)
|
||||||
|
}
|
||||||
|
defer cfgFile.Close()
|
||||||
|
|
||||||
|
opts := &Options{ShellDir: shellDir}
|
||||||
|
|
||||||
|
appendConfig(opts, cfgFile, nil, nil, "nonexistent.toml")
|
||||||
|
|
||||||
|
cfgFile.Close()
|
||||||
|
output, err := os.ReadFile(outFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read output: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(output) != 0 {
|
||||||
|
t.Errorf("expected no config when file doesn't exist, got: %q", string(output))
|
||||||
|
}
|
||||||
|
}
|
||||||
242
core/internal/mocks/utils/mock_AppChecker.go
Normal file
242
core/internal/mocks/utils/mock_AppChecker.go
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
// Code generated by mockery v2.53.5. DO NOT EDIT.
|
||||||
|
|
||||||
|
package mocks_utils
|
||||||
|
|
||||||
|
import mock "github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
|
// MockAppChecker is an autogenerated mock type for the AppChecker type
|
||||||
|
type MockAppChecker struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockAppChecker_Expecter struct {
|
||||||
|
mock *mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_m *MockAppChecker) EXPECT() *MockAppChecker_Expecter {
|
||||||
|
return &MockAppChecker_Expecter{mock: &_m.Mock}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnyCommandExists provides a mock function with given fields: cmds
|
||||||
|
func (_m *MockAppChecker) AnyCommandExists(cmds ...string) bool {
|
||||||
|
_va := make([]interface{}, len(cmds))
|
||||||
|
for _i := range cmds {
|
||||||
|
_va[_i] = cmds[_i]
|
||||||
|
}
|
||||||
|
var _ca []interface{}
|
||||||
|
_ca = append(_ca, _va...)
|
||||||
|
ret := _m.Called(_ca...)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for AnyCommandExists")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 bool
|
||||||
|
if rf, ok := ret.Get(0).(func(...string) bool); ok {
|
||||||
|
r0 = rf(cmds...)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockAppChecker_AnyCommandExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AnyCommandExists'
|
||||||
|
type MockAppChecker_AnyCommandExists_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnyCommandExists is a helper method to define mock.On call
|
||||||
|
// - cmds ...string
|
||||||
|
func (_e *MockAppChecker_Expecter) AnyCommandExists(cmds ...interface{}) *MockAppChecker_AnyCommandExists_Call {
|
||||||
|
return &MockAppChecker_AnyCommandExists_Call{Call: _e.mock.On("AnyCommandExists",
|
||||||
|
append([]interface{}{}, cmds...)...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockAppChecker_AnyCommandExists_Call) Run(run func(cmds ...string)) *MockAppChecker_AnyCommandExists_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
variadicArgs := make([]string, len(args)-0)
|
||||||
|
for i, a := range args[0:] {
|
||||||
|
if a != nil {
|
||||||
|
variadicArgs[i] = a.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run(variadicArgs...)
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockAppChecker_AnyCommandExists_Call) Return(_a0 bool) *MockAppChecker_AnyCommandExists_Call {
|
||||||
|
_c.Call.Return(_a0)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockAppChecker_AnyCommandExists_Call) RunAndReturn(run func(...string) bool) *MockAppChecker_AnyCommandExists_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnyFlatpakExists provides a mock function with given fields: flatpaks
|
||||||
|
func (_m *MockAppChecker) AnyFlatpakExists(flatpaks ...string) bool {
|
||||||
|
_va := make([]interface{}, len(flatpaks))
|
||||||
|
for _i := range flatpaks {
|
||||||
|
_va[_i] = flatpaks[_i]
|
||||||
|
}
|
||||||
|
var _ca []interface{}
|
||||||
|
_ca = append(_ca, _va...)
|
||||||
|
ret := _m.Called(_ca...)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for AnyFlatpakExists")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 bool
|
||||||
|
if rf, ok := ret.Get(0).(func(...string) bool); ok {
|
||||||
|
r0 = rf(flatpaks...)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockAppChecker_AnyFlatpakExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AnyFlatpakExists'
|
||||||
|
type MockAppChecker_AnyFlatpakExists_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnyFlatpakExists is a helper method to define mock.On call
|
||||||
|
// - flatpaks ...string
|
||||||
|
func (_e *MockAppChecker_Expecter) AnyFlatpakExists(flatpaks ...interface{}) *MockAppChecker_AnyFlatpakExists_Call {
|
||||||
|
return &MockAppChecker_AnyFlatpakExists_Call{Call: _e.mock.On("AnyFlatpakExists",
|
||||||
|
append([]interface{}{}, flatpaks...)...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockAppChecker_AnyFlatpakExists_Call) Run(run func(flatpaks ...string)) *MockAppChecker_AnyFlatpakExists_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
variadicArgs := make([]string, len(args)-0)
|
||||||
|
for i, a := range args[0:] {
|
||||||
|
if a != nil {
|
||||||
|
variadicArgs[i] = a.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run(variadicArgs...)
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockAppChecker_AnyFlatpakExists_Call) Return(_a0 bool) *MockAppChecker_AnyFlatpakExists_Call {
|
||||||
|
_c.Call.Return(_a0)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockAppChecker_AnyFlatpakExists_Call) RunAndReturn(run func(...string) bool) *MockAppChecker_AnyFlatpakExists_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommandExists provides a mock function with given fields: cmd
|
||||||
|
func (_m *MockAppChecker) CommandExists(cmd string) bool {
|
||||||
|
ret := _m.Called(cmd)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for CommandExists")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 bool
|
||||||
|
if rf, ok := ret.Get(0).(func(string) bool); ok {
|
||||||
|
r0 = rf(cmd)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockAppChecker_CommandExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CommandExists'
|
||||||
|
type MockAppChecker_CommandExists_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommandExists is a helper method to define mock.On call
|
||||||
|
// - cmd string
|
||||||
|
func (_e *MockAppChecker_Expecter) CommandExists(cmd interface{}) *MockAppChecker_CommandExists_Call {
|
||||||
|
return &MockAppChecker_CommandExists_Call{Call: _e.mock.On("CommandExists", cmd)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockAppChecker_CommandExists_Call) Run(run func(cmd string)) *MockAppChecker_CommandExists_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(string))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockAppChecker_CommandExists_Call) Return(_a0 bool) *MockAppChecker_CommandExists_Call {
|
||||||
|
_c.Call.Return(_a0)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockAppChecker_CommandExists_Call) RunAndReturn(run func(string) bool) *MockAppChecker_CommandExists_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlatpakExists provides a mock function with given fields: name
|
||||||
|
func (_m *MockAppChecker) FlatpakExists(name string) bool {
|
||||||
|
ret := _m.Called(name)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for FlatpakExists")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 bool
|
||||||
|
if rf, ok := ret.Get(0).(func(string) bool); ok {
|
||||||
|
r0 = rf(name)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockAppChecker_FlatpakExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FlatpakExists'
|
||||||
|
type MockAppChecker_FlatpakExists_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlatpakExists is a helper method to define mock.On call
|
||||||
|
// - name string
|
||||||
|
func (_e *MockAppChecker_Expecter) FlatpakExists(name interface{}) *MockAppChecker_FlatpakExists_Call {
|
||||||
|
return &MockAppChecker_FlatpakExists_Call{Call: _e.mock.On("FlatpakExists", name)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockAppChecker_FlatpakExists_Call) Run(run func(name string)) *MockAppChecker_FlatpakExists_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(string))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockAppChecker_FlatpakExists_Call) Return(_a0 bool) *MockAppChecker_FlatpakExists_Call {
|
||||||
|
_c.Call.Return(_a0)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockAppChecker_FlatpakExists_Call) RunAndReturn(run func(string) bool) *MockAppChecker_FlatpakExists_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockAppChecker creates a new instance of MockAppChecker. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||||
|
// The first argument is typically a *testing.T value.
|
||||||
|
func NewMockAppChecker(t interface {
|
||||||
|
mock.TestingT
|
||||||
|
Cleanup(func())
|
||||||
|
}) *MockAppChecker {
|
||||||
|
mock := &MockAppChecker{}
|
||||||
|
mock.Mock.Test(t)
|
||||||
|
|
||||||
|
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
||||||
@@ -141,7 +141,7 @@ func (r *RegionSelector) setupKeyboardHandlers() {
|
|||||||
for _, os := range r.surfaces {
|
for _, os := range r.surfaces {
|
||||||
r.redrawSurface(os)
|
r.redrawSurface(os)
|
||||||
}
|
}
|
||||||
case 28, 57:
|
case 28, 57, 96:
|
||||||
if r.selection.hasSelection {
|
if r.selection.hasSelection {
|
||||||
r.finishSelection()
|
r.finishSelection()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
|||||||
func handleOpen(conn net.Conn, req models.Request, manager *Manager) {
|
func handleOpen(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
log.Infof("AppPicker: Received %s request with params: %+v", req.Method, req.Params)
|
log.Infof("AppPicker: Received %s request with params: %+v", req.Method, req.Params)
|
||||||
|
|
||||||
target, ok := req.Params["target"].(string)
|
target, ok := models.Get[string](req, "target")
|
||||||
if !ok {
|
if !ok {
|
||||||
target, ok = req.Params["url"].(string)
|
target, ok = models.Get[string](req, "url")
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Warnf("AppPicker: Invalid target parameter in request")
|
log.Warnf("AppPicker: Invalid target parameter in request")
|
||||||
models.RespondError(conn, req.ID, "invalid target parameter")
|
models.RespondError(conn, req.ID, "invalid target parameter")
|
||||||
@@ -31,14 +31,11 @@ func handleOpen(conn net.Conn, req models.Request, manager *Manager) {
|
|||||||
|
|
||||||
event := OpenEvent{
|
event := OpenEvent{
|
||||||
Target: target,
|
Target: target,
|
||||||
RequestType: "url",
|
RequestType: models.GetOr(req, "requestType", "url"),
|
||||||
|
MimeType: models.GetOr(req, "mimeType", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
if mimeType, ok := req.Params["mimeType"].(string); ok {
|
if categories, ok := models.Get[[]any](req, "categories"); ok {
|
||||||
event.MimeType = mimeType
|
|
||||||
}
|
|
||||||
|
|
||||||
if categories, ok := req.Params["categories"].([]any); ok {
|
|
||||||
event.Categories = make([]string, 0, len(categories))
|
event.Categories = make([]string, 0, len(categories))
|
||||||
for _, cat := range categories {
|
for _, cat := range categories {
|
||||||
if catStr, ok := cat.(string); ok {
|
if catStr, ok := cat.(string); ok {
|
||||||
@@ -47,10 +44,6 @@ func handleOpen(conn net.Conn, req models.Request, manager *Manager) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if requestType, ok := req.Params["requestType"].(string); ok {
|
|
||||||
event.RequestType = requestType
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("AppPicker: Broadcasting event: %+v", event)
|
log.Infof("AppPicker: Broadcasting event: %+v", event)
|
||||||
manager.RequestOpen(event)
|
manager.RequestOpen(event)
|
||||||
models.Respond(conn, req.ID, "ok")
|
models.Respond(conn, req.ID, "ok")
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
switch req.Method {
|
switch req.Method {
|
||||||
case "browser.open":
|
case "browser.open":
|
||||||
url, ok := req.Params["url"].(string)
|
url, ok := models.Get[string](req, "url")
|
||||||
if !ok {
|
if !ok {
|
||||||
models.RespondError(conn, req.ID, "invalid url parameter")
|
models.RespondError(conn, req.ID, "invalid url parameter")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -168,14 +168,14 @@ func handleSearch(conn net.Conn, req models.Request, m *Manager) {
|
|||||||
Offset: params.IntOpt(req.Params, "offset", 0),
|
Offset: params.IntOpt(req.Params, "offset", 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
if img, ok := req.Params["isImage"].(bool); ok {
|
if img, ok := models.Get[bool](req, "isImage"); ok {
|
||||||
p.IsImage = &img
|
p.IsImage = &img
|
||||||
}
|
}
|
||||||
if b, ok := req.Params["before"].(float64); ok {
|
if b, ok := models.Get[float64](req, "before"); ok {
|
||||||
v := int64(b)
|
v := int64(b)
|
||||||
p.Before = &v
|
p.Before = &v
|
||||||
}
|
}
|
||||||
if a, ok := req.Params["after"].(float64); ok {
|
if a, ok := models.Get[float64](req, "after"); ok {
|
||||||
v := int64(a)
|
v := int64(a)
|
||||||
p.After = &v
|
p.After = &v
|
||||||
}
|
}
|
||||||
@@ -190,27 +190,21 @@ func handleGetConfig(conn net.Conn, req models.Request, m *Manager) {
|
|||||||
func handleSetConfig(conn net.Conn, req models.Request, m *Manager) {
|
func handleSetConfig(conn net.Conn, req models.Request, m *Manager) {
|
||||||
cfg := m.GetConfig()
|
cfg := m.GetConfig()
|
||||||
|
|
||||||
if _, ok := req.Params["maxHistory"]; ok {
|
if v, ok := models.Get[float64](req, "maxHistory"); ok {
|
||||||
cfg.MaxHistory = params.IntOpt(req.Params, "maxHistory", cfg.MaxHistory)
|
cfg.MaxHistory = int(v)
|
||||||
}
|
}
|
||||||
if _, ok := req.Params["maxEntrySize"]; ok {
|
if v, ok := models.Get[float64](req, "maxEntrySize"); ok {
|
||||||
cfg.MaxEntrySize = int64(params.IntOpt(req.Params, "maxEntrySize", int(cfg.MaxEntrySize)))
|
cfg.MaxEntrySize = int64(v)
|
||||||
}
|
}
|
||||||
if _, ok := req.Params["autoClearDays"]; ok {
|
if v, ok := models.Get[float64](req, "autoClearDays"); ok {
|
||||||
cfg.AutoClearDays = params.IntOpt(req.Params, "autoClearDays", cfg.AutoClearDays)
|
cfg.AutoClearDays = int(v)
|
||||||
}
|
}
|
||||||
if v, ok := req.Params["clearAtStartup"].(bool); ok {
|
if v, ok := models.Get[bool](req, "clearAtStartup"); ok {
|
||||||
cfg.ClearAtStartup = v
|
cfg.ClearAtStartup = v
|
||||||
}
|
}
|
||||||
if v, ok := req.Params["disabled"].(bool); ok {
|
if v, ok := models.Get[bool](req, "disabled"); ok {
|
||||||
cfg.Disabled = v
|
cfg.Disabled = v
|
||||||
}
|
}
|
||||||
if v, ok := req.Params["disableHistory"].(bool); ok {
|
|
||||||
cfg.DisableHistory = v
|
|
||||||
}
|
|
||||||
if v, ok := req.Params["disablePersist"].(bool); ok {
|
|
||||||
cfg.DisablePersist = v
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.SetConfig(cfg); err != nil {
|
if err := m.SetConfig(cfg); err != nil {
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
|||||||
@@ -11,14 +11,16 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"hash/fnv"
|
||||||
|
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
_ "golang.org/x/image/bmp"
|
_ "golang.org/x/image/bmp"
|
||||||
_ "golang.org/x/image/tiff"
|
_ "golang.org/x/image/tiff"
|
||||||
"hash/fnv"
|
|
||||||
|
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
|
|
||||||
@@ -28,11 +30,12 @@ import (
|
|||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error) {
|
// These mime types wont be stored in history
|
||||||
if config.Disabled {
|
var sensitiveMimeTypes = []string{
|
||||||
return nil, fmt.Errorf("clipboard disabled in config")
|
"x-kde-passwordManagerHint",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error) {
|
||||||
display := wlCtx.Display()
|
display := wlCtx.Display()
|
||||||
dbPath, err := getDBPath()
|
dbPath, err := getDBPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -54,8 +57,10 @@ func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error)
|
|||||||
dbPath: dbPath,
|
dbPath: dbPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.setupRegistry(); err != nil {
|
if !config.Disabled {
|
||||||
return nil, err
|
if err := m.setupRegistry(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m.notifierWg.Add(1)
|
m.notifierWg.Add(1)
|
||||||
@@ -63,17 +68,17 @@ func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error)
|
|||||||
|
|
||||||
go m.watchConfig()
|
go m.watchConfig()
|
||||||
|
|
||||||
if !config.DisableHistory {
|
db, err := openDB(dbPath)
|
||||||
db, err := openDB(dbPath)
|
if err != nil {
|
||||||
if err != nil {
|
return nil, fmt.Errorf("failed to open db: %w", err)
|
||||||
return nil, fmt.Errorf("failed to open db: %w", err)
|
}
|
||||||
}
|
m.db = db
|
||||||
m.db = db
|
|
||||||
|
|
||||||
if err := m.migrateHashes(); err != nil {
|
if err := m.migrateHashes(); err != nil {
|
||||||
log.Errorf("Failed to migrate hashes: %v", err)
|
log.Errorf("Failed to migrate hashes: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !config.Disabled {
|
||||||
if config.ClearAtStartup {
|
if config.ClearAtStartup {
|
||||||
if err := m.clearHistoryInternal(); err != nil {
|
if err := m.clearHistoryInternal(); err != nil {
|
||||||
log.Errorf("Failed to clear history at startup: %v", err)
|
log.Errorf("Failed to clear history at startup: %v", err)
|
||||||
@@ -90,7 +95,7 @@ func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error)
|
|||||||
m.alive = true
|
m.alive = true
|
||||||
m.updateState()
|
m.updateState()
|
||||||
|
|
||||||
if m.dataControlMgr != nil && m.seat != nil {
|
if !config.Disabled && m.dataControlMgr != nil && m.seat != nil {
|
||||||
m.setupDataDeviceSync()
|
m.setupDataDeviceSync()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +258,10 @@ func (m *Manager) setupDataDeviceSync() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m.hasSensitiveMimeType(mimes) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
preferredMime := m.selectMimeType(mimes)
|
preferredMime := m.selectMimeType(mimes)
|
||||||
if preferredMime == "" {
|
if preferredMime == "" {
|
||||||
return
|
return
|
||||||
@@ -315,14 +324,10 @@ func (m *Manager) readAndStore(r *os.File, mimeType string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cfg.DisableHistory && m.db != nil {
|
if !cfg.Disabled && m.db != nil {
|
||||||
m.storeClipboardEntry(data, mimeType)
|
m.storeClipboardEntry(data, mimeType)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cfg.DisablePersist {
|
|
||||||
m.persistClipboard([]string{mimeType}, map[string][]byte{mimeType: data})
|
|
||||||
}
|
|
||||||
|
|
||||||
m.updateState()
|
m.updateState()
|
||||||
m.notifySubscribers()
|
m.notifySubscribers()
|
||||||
}
|
}
|
||||||
@@ -348,105 +353,6 @@ func (m *Manager) storeClipboardEntry(data []byte, mimeType string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) persistClipboard(mimeTypes []string, data map[string][]byte) {
|
|
||||||
m.persistMutex.Lock()
|
|
||||||
m.persistMimeTypes = mimeTypes
|
|
||||||
m.persistData = data
|
|
||||||
m.persistMutex.Unlock()
|
|
||||||
|
|
||||||
m.post(func() {
|
|
||||||
m.takePersistOwnership()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) takePersistOwnership() {
|
|
||||||
if m.dataControlMgr == nil || m.dataDevice == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.getConfig().DisablePersist {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
m.persistMutex.RLock()
|
|
||||||
mimeTypes := m.persistMimeTypes
|
|
||||||
m.persistMutex.RUnlock()
|
|
||||||
|
|
||||||
if len(mimeTypes) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dataMgr := m.dataControlMgr.(*ext_data_control.ExtDataControlManagerV1)
|
|
||||||
|
|
||||||
source, err := dataMgr.CreateDataSource()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Failed to create persist source: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, mime := range mimeTypes {
|
|
||||||
if err := source.Offer(mime); err != nil {
|
|
||||||
log.Errorf("Failed to offer mime type %s: %v", mime, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
|
||||||
fd := e.Fd
|
|
||||||
defer syscall.Close(fd)
|
|
||||||
|
|
||||||
m.persistMutex.RLock()
|
|
||||||
d := m.persistData[e.MimeType]
|
|
||||||
m.persistMutex.RUnlock()
|
|
||||||
|
|
||||||
if len(d) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
file := os.NewFile(uintptr(fd), "clipboard-pipe")
|
|
||||||
defer file.Close()
|
|
||||||
file.Write(d)
|
|
||||||
})
|
|
||||||
|
|
||||||
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
|
|
||||||
m.ownerLock.Lock()
|
|
||||||
m.isOwner = false
|
|
||||||
m.ownerLock.Unlock()
|
|
||||||
})
|
|
||||||
|
|
||||||
if m.currentSource != nil {
|
|
||||||
oldSource := m.currentSource.(*ext_data_control.ExtDataControlSourceV1)
|
|
||||||
oldSource.Destroy()
|
|
||||||
}
|
|
||||||
m.currentSource = source
|
|
||||||
|
|
||||||
device := m.dataDevice.(*ext_data_control.ExtDataControlDeviceV1)
|
|
||||||
if err := device.SetSelection(source); err != nil {
|
|
||||||
log.Errorf("Failed to set persist selection: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
m.ownerLock.Lock()
|
|
||||||
m.isOwner = true
|
|
||||||
m.ownerLock.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) releaseOwnership() {
|
|
||||||
m.ownerLock.Lock()
|
|
||||||
m.isOwner = false
|
|
||||||
m.ownerLock.Unlock()
|
|
||||||
|
|
||||||
m.persistMutex.Lock()
|
|
||||||
m.persistData = nil
|
|
||||||
m.persistMimeTypes = nil
|
|
||||||
m.persistMutex.Unlock()
|
|
||||||
|
|
||||||
if m.currentSource != nil {
|
|
||||||
source := m.currentSource.(*ext_data_control.ExtDataControlSourceV1)
|
|
||||||
source.Destroy()
|
|
||||||
m.currentSource = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) storeEntry(entry Entry) error {
|
func (m *Manager) storeEntry(entry Entry) error {
|
||||||
if m.db == nil {
|
if m.db == nil {
|
||||||
return fmt.Errorf("database not available")
|
return fmt.Errorf("database not available")
|
||||||
@@ -495,6 +401,9 @@ func (m *Manager) deduplicateInTx(b *bolt.Bucket, hash uint64) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) trimLengthInTx(b *bolt.Bucket) error {
|
func (m *Manager) trimLengthInTx(b *bolt.Bucket) error {
|
||||||
|
if m.config.MaxHistory < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
var count int
|
var count int
|
||||||
for k, _ := c.Last(); k != nil; k, _ = c.Prev() {
|
for k, _ := c.Last(); k != nil; k, _ = c.Prev() {
|
||||||
@@ -592,6 +501,12 @@ func extractHash(data []byte) uint64 {
|
|||||||
return binary.BigEndian.Uint64(data[len(data)-8:])
|
return binary.BigEndian.Uint64(data[len(data)-8:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) hasSensitiveMimeType(mimes []string) bool {
|
||||||
|
return slices.ContainsFunc(mimes, func(mime string) bool {
|
||||||
|
return slices.Contains(sensitiveMimeTypes, mime)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) selectMimeType(mimes []string) string {
|
func (m *Manager) selectMimeType(mimes []string) string {
|
||||||
preferredTypes := []string{
|
preferredTypes := []string{
|
||||||
"text/plain;charset=utf-8",
|
"text/plain;charset=utf-8",
|
||||||
@@ -1294,29 +1209,13 @@ func (m *Manager) applyConfigChange(newCfg Config) {
|
|||||||
m.config = newCfg
|
m.config = newCfg
|
||||||
m.configMutex.Unlock()
|
m.configMutex.Unlock()
|
||||||
|
|
||||||
if newCfg.DisableHistory && !oldCfg.DisableHistory && m.db != nil {
|
switch {
|
||||||
log.Info("Clipboard history disabled, closing database")
|
case newCfg.Disabled && !oldCfg.Disabled:
|
||||||
m.db.Close()
|
log.Info("Clipboard tracking disabled")
|
||||||
m.db = nil
|
case !newCfg.Disabled && oldCfg.Disabled:
|
||||||
|
log.Info("Clipboard tracking enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !newCfg.DisableHistory && oldCfg.DisableHistory && m.db == nil {
|
|
||||||
log.Info("Clipboard history enabled, opening database")
|
|
||||||
if db, err := openDB(m.dbPath); err == nil {
|
|
||||||
m.db = db
|
|
||||||
} else {
|
|
||||||
log.Errorf("Failed to reopen database: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if newCfg.DisablePersist && !oldCfg.DisablePersist {
|
|
||||||
log.Info("Clipboard persist disabled, releasing ownership")
|
|
||||||
m.releaseOwnership()
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("Clipboard config reloaded: disableHistory=%v disablePersist=%v",
|
|
||||||
newCfg.DisableHistory, newCfg.DisablePersist)
|
|
||||||
|
|
||||||
m.updateState()
|
m.updateState()
|
||||||
m.notifySubscribers()
|
m.notifySubscribers()
|
||||||
}
|
}
|
||||||
@@ -1324,8 +1223,8 @@ func (m *Manager) applyConfigChange(newCfg Config) {
|
|||||||
func (m *Manager) StoreData(data []byte, mimeType string) error {
|
func (m *Manager) StoreData(data []byte, mimeType string) error {
|
||||||
cfg := m.getConfig()
|
cfg := m.getConfig()
|
||||||
|
|
||||||
if cfg.DisableHistory {
|
if cfg.Disabled {
|
||||||
return fmt.Errorf("clipboard history disabled")
|
return fmt.Errorf("clipboard tracking disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.db == nil {
|
if m.db == nil {
|
||||||
|
|||||||
@@ -457,8 +457,6 @@ func TestDefaultConfig(t *testing.T) {
|
|||||||
assert.Equal(t, 0, cfg.AutoClearDays)
|
assert.Equal(t, 0, cfg.AutoClearDays)
|
||||||
assert.False(t, cfg.ClearAtStartup)
|
assert.False(t, cfg.ClearAtStartup)
|
||||||
assert.False(t, cfg.Disabled)
|
assert.False(t, cfg.Disabled)
|
||||||
assert.False(t, cfg.DisableHistory)
|
|
||||||
assert.True(t, cfg.DisablePersist)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestManager_PostDelegatesToWlContext(t *testing.T) {
|
func TestManager_PostDelegatesToWlContext(t *testing.T) {
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ type Config struct {
|
|||||||
MaxEntrySize int64 `json:"maxEntrySize"`
|
MaxEntrySize int64 `json:"maxEntrySize"`
|
||||||
AutoClearDays int `json:"autoClearDays"`
|
AutoClearDays int `json:"autoClearDays"`
|
||||||
ClearAtStartup bool `json:"clearAtStartup"`
|
ClearAtStartup bool `json:"clearAtStartup"`
|
||||||
|
Disabled bool `json:"disabled"`
|
||||||
Disabled bool `json:"disabled"`
|
|
||||||
DisableHistory bool `json:"disableHistory"`
|
|
||||||
DisablePersist bool `json:"disablePersist"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultConfig() Config {
|
func DefaultConfig() Config {
|
||||||
@@ -30,7 +27,6 @@ func DefaultConfig() Config {
|
|||||||
MaxEntrySize: 5 * 1024 * 1024,
|
MaxEntrySize: 5 * 1024 * 1024,
|
||||||
AutoClearDays: 0,
|
AutoClearDays: 0,
|
||||||
ClearAtStartup: false,
|
ClearAtStartup: false,
|
||||||
DisablePersist: true,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package cups
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -156,9 +158,42 @@ func (m *Manager) PurgeJobs(printerName string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveIPFromURI(uri string) string {
|
||||||
|
parsed, err := url.Parse(uri)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
host := parsed.Hostname()
|
||||||
|
if host == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if ip := net.ParseIP(host); ip != nil {
|
||||||
|
return ip.String()
|
||||||
|
}
|
||||||
|
addrs, err := net.LookupIP(host)
|
||||||
|
if err != nil || len(addrs) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for _, addr := range addrs {
|
||||||
|
if v4 := addr.To4(); v4 != nil {
|
||||||
|
return v4.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addrs[0].String()
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) GetDevices() ([]Device, error) {
|
func (m *Manager) GetDevices() ([]Device, error) {
|
||||||
if m.pkHelper != nil {
|
if m.pkHelper != nil {
|
||||||
return m.pkHelper.DevicesGet(10, 0, nil, nil)
|
devices, err := m.pkHelper.DevicesGet(10, 0, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for i := range devices {
|
||||||
|
if devices[i].Class == "network" {
|
||||||
|
devices[i].IP = resolveIPFromURI(devices[i].URI)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return devices, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceAttrs, err := m.client.GetDevices()
|
deviceAttrs, err := m.client.GetDevices()
|
||||||
@@ -176,6 +211,9 @@ func (m *Manager) GetDevices() ([]Device, error) {
|
|||||||
ID: getStringAttr(attrs, "device-id"),
|
ID: getStringAttr(attrs, "device-id"),
|
||||||
Location: getStringAttr(attrs, "device-location"),
|
Location: getStringAttr(attrs, "device-location"),
|
||||||
}
|
}
|
||||||
|
if device.Class == "network" {
|
||||||
|
device.IP = resolveIPFromURI(uri)
|
||||||
|
}
|
||||||
devices = append(devices, device)
|
devices = append(devices, device)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ type Device struct {
|
|||||||
MakeModel string `json:"makeModel"`
|
MakeModel string `json:"makeModel"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Location string `json:"location"`
|
Location string `json:"location"`
|
||||||
|
IP string `json:"ip,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PPD struct {
|
type PPD struct {
|
||||||
|
|||||||
@@ -41,19 +41,19 @@ func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleSetTags(conn net.Conn, req models.Request, manager *Manager) {
|
func handleSetTags(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
output, ok := req.Params["output"].(string)
|
output, ok := models.Get[string](req, "output")
|
||||||
if !ok {
|
if !ok {
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
|
models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tagmask, ok := req.Params["tagmask"].(float64)
|
tagmask, ok := models.Get[float64](req, "tagmask")
|
||||||
if !ok {
|
if !ok {
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'tagmask' parameter")
|
models.RespondError(conn, req.ID, "missing or invalid 'tagmask' parameter")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleTagset, ok := req.Params["toggleTagset"].(float64)
|
toggleTagset, ok := models.Get[float64](req, "toggleTagset")
|
||||||
if !ok {
|
if !ok {
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'toggleTagset' parameter")
|
models.RespondError(conn, req.ID, "missing or invalid 'toggleTagset' parameter")
|
||||||
return
|
return
|
||||||
@@ -68,19 +68,19 @@ func handleSetTags(conn net.Conn, req models.Request, manager *Manager) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleSetClientTags(conn net.Conn, req models.Request, manager *Manager) {
|
func handleSetClientTags(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
output, ok := req.Params["output"].(string)
|
output, ok := models.Get[string](req, "output")
|
||||||
if !ok {
|
if !ok {
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
|
models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
andTags, ok := req.Params["andTags"].(float64)
|
andTags, ok := models.Get[float64](req, "andTags")
|
||||||
if !ok {
|
if !ok {
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'andTags' parameter")
|
models.RespondError(conn, req.ID, "missing or invalid 'andTags' parameter")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
xorTags, ok := req.Params["xorTags"].(float64)
|
xorTags, ok := models.Get[float64](req, "xorTags")
|
||||||
if !ok {
|
if !ok {
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'xorTags' parameter")
|
models.RespondError(conn, req.ID, "missing or invalid 'xorTags' parameter")
|
||||||
return
|
return
|
||||||
@@ -95,13 +95,13 @@ func handleSetClientTags(conn net.Conn, req models.Request, manager *Manager) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleSetLayout(conn net.Conn, req models.Request, manager *Manager) {
|
func handleSetLayout(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
output, ok := req.Params["output"].(string)
|
output, ok := models.Get[string](req, "output")
|
||||||
if !ok {
|
if !ok {
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
|
models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
index, ok := req.Params["index"].(float64)
|
index, ok := models.Get[float64](req, "index")
|
||||||
if !ok {
|
if !ok {
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'index' parameter")
|
models.RespondError(conn, req.ID, "missing or invalid 'index' parameter")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -43,12 +43,8 @@ func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleActivateWorkspace(conn net.Conn, req models.Request, manager *Manager) {
|
func handleActivateWorkspace(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
groupID, ok := req.Params["groupID"].(string)
|
groupID := models.GetOr(req, "groupID", "")
|
||||||
if !ok {
|
workspaceID, ok := models.Get[string](req, "workspaceID")
|
||||||
groupID = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
workspaceID, ok := req.Params["workspaceID"].(string)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'workspaceID' parameter")
|
models.RespondError(conn, req.ID, "missing or invalid 'workspaceID' parameter")
|
||||||
return
|
return
|
||||||
@@ -63,12 +59,8 @@ func handleActivateWorkspace(conn net.Conn, req models.Request, manager *Manager
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleDeactivateWorkspace(conn net.Conn, req models.Request, manager *Manager) {
|
func handleDeactivateWorkspace(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
groupID, ok := req.Params["groupID"].(string)
|
groupID := models.GetOr(req, "groupID", "")
|
||||||
if !ok {
|
workspaceID, ok := models.Get[string](req, "workspaceID")
|
||||||
groupID = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
workspaceID, ok := req.Params["workspaceID"].(string)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'workspaceID' parameter")
|
models.RespondError(conn, req.ID, "missing or invalid 'workspaceID' parameter")
|
||||||
return
|
return
|
||||||
@@ -83,12 +75,8 @@ func handleDeactivateWorkspace(conn net.Conn, req models.Request, manager *Manag
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleRemoveWorkspace(conn net.Conn, req models.Request, manager *Manager) {
|
func handleRemoveWorkspace(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
groupID, ok := req.Params["groupID"].(string)
|
groupID := models.GetOr(req, "groupID", "")
|
||||||
if !ok {
|
workspaceID, ok := models.Get[string](req, "workspaceID")
|
||||||
groupID = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
workspaceID, ok := req.Params["workspaceID"].(string)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'workspaceID' parameter")
|
models.RespondError(conn, req.ID, "missing or invalid 'workspaceID' parameter")
|
||||||
return
|
return
|
||||||
@@ -103,13 +91,13 @@ func handleRemoveWorkspace(conn net.Conn, req models.Request, manager *Manager)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleCreateWorkspace(conn net.Conn, req models.Request, manager *Manager) {
|
func handleCreateWorkspace(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
groupID, ok := req.Params["groupID"].(string)
|
groupID, ok := models.Get[string](req, "groupID")
|
||||||
if !ok {
|
if !ok {
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'groupID' parameter")
|
models.RespondError(conn, req.ID, "missing or invalid 'groupID' parameter")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
workspaceName, ok := req.Params["name"].(string)
|
workspaceName, ok := models.Get[string](req, "name")
|
||||||
if !ok {
|
if !ok {
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
|
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -15,37 +15,23 @@ type MatugenQueueResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleMatugenQueue(conn net.Conn, req models.Request) {
|
func handleMatugenQueue(conn net.Conn, req models.Request) {
|
||||||
getString := func(key string) string {
|
|
||||||
if v, ok := req.Params[key].(string); ok {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
getBool := func(key string, def bool) bool {
|
|
||||||
if v, ok := req.Params[key].(bool); ok {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := matugen.Options{
|
opts := matugen.Options{
|
||||||
StateDir: getString("stateDir"),
|
StateDir: models.GetOr(req, "stateDir", ""),
|
||||||
ShellDir: getString("shellDir"),
|
ShellDir: models.GetOr(req, "shellDir", ""),
|
||||||
ConfigDir: getString("configDir"),
|
ConfigDir: models.GetOr(req, "configDir", ""),
|
||||||
Kind: getString("kind"),
|
Kind: models.GetOr(req, "kind", ""),
|
||||||
Value: getString("value"),
|
Value: models.GetOr(req, "value", ""),
|
||||||
Mode: getString("mode"),
|
Mode: matugen.ColorMode(models.GetOr(req, "mode", "")),
|
||||||
IconTheme: getString("iconTheme"),
|
IconTheme: models.GetOr(req, "iconTheme", ""),
|
||||||
MatugenType: getString("matugenType"),
|
MatugenType: models.GetOr(req, "matugenType", ""),
|
||||||
RunUserTemplates: getBool("runUserTemplates", true),
|
RunUserTemplates: models.GetOr(req, "runUserTemplates", true),
|
||||||
StockColors: getString("stockColors"),
|
StockColors: models.GetOr(req, "stockColors", ""),
|
||||||
SyncModeWithPortal: getBool("syncModeWithPortal", false),
|
SyncModeWithPortal: models.GetOr(req, "syncModeWithPortal", false),
|
||||||
TerminalsAlwaysDark: getBool("terminalsAlwaysDark", false),
|
TerminalsAlwaysDark: models.GetOr(req, "terminalsAlwaysDark", false),
|
||||||
SkipTemplates: getString("skipTemplates"),
|
SkipTemplates: models.GetOr(req, "skipTemplates", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
wait := getBool("wait", true)
|
wait := models.GetOr(req, "wait", true)
|
||||||
|
|
||||||
queue := matugen.GetQueue()
|
queue := matugen.GetQueue()
|
||||||
resultCh := queue.Submit(opts)
|
resultCh := queue.Submit(opts)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Request struct {
|
type Request struct {
|
||||||
@@ -13,6 +14,15 @@ type Request struct {
|
|||||||
Params map[string]any `json:"params,omitempty"`
|
Params map[string]any `json:"params,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Get[T any](r Request, key string) (T, bool) {
|
||||||
|
v, err := params.Get[T](r.Params, key)
|
||||||
|
return v, err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOr[T any](r Request, key string, def T) T {
|
||||||
|
return params.GetOpt(r.Params, key, def)
|
||||||
|
}
|
||||||
|
|
||||||
type Response[T any] struct {
|
type Response[T any] struct {
|
||||||
ID int `json:"id,omitempty"`
|
ID int `json:"id,omitempty"`
|
||||||
Result *T `json:"result,omitempty"`
|
Result *T `json:"result,omitempty"`
|
||||||
|
|||||||
52
core/internal/server/models/types_test.go
Normal file
52
core/internal/server/models/types_test.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestGet(t *testing.T) {
|
||||||
|
req := Request{Params: map[string]any{"name": "test", "count": 42, "enabled": true}}
|
||||||
|
|
||||||
|
name, ok := Get[string](req, "name")
|
||||||
|
if !ok || name != "test" {
|
||||||
|
t.Errorf("Get[string] = %q, %v; want 'test', true", name, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
count, ok := Get[int](req, "count")
|
||||||
|
if !ok || count != 42 {
|
||||||
|
t.Errorf("Get[int] = %d, %v; want 42, true", count, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled, ok := Get[bool](req, "enabled")
|
||||||
|
if !ok || !enabled {
|
||||||
|
t.Errorf("Get[bool] = %v, %v; want true, true", enabled, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok = Get[string](req, "missing")
|
||||||
|
if ok {
|
||||||
|
t.Error("Get missing key should return false")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok = Get[int](req, "name")
|
||||||
|
if ok {
|
||||||
|
t.Error("Get wrong type should return false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetOr(t *testing.T) {
|
||||||
|
req := Request{Params: map[string]any{"name": "test", "enabled": true}}
|
||||||
|
|
||||||
|
if v := GetOr(req, "name", "default"); v != "test" {
|
||||||
|
t.Errorf("GetOr existing = %q; want 'test'", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := GetOr(req, "missing", "default"); v != "default" {
|
||||||
|
t.Errorf("GetOr missing = %q; want 'default'", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := GetOr(req, "enabled", false); !v {
|
||||||
|
t.Errorf("GetOr bool = %v; want true", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := GetOr(req, "name", 0); v != 0 {
|
||||||
|
t.Errorf("GetOr wrong type = %d; want 0 (default)", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -233,6 +233,9 @@ func (a *SecretAgent) GetSecrets(
|
|||||||
if a.manager != nil && connType == "802-11-wireless" && a.manager.WasRecentlyFailed(ssid) {
|
if a.manager != nil && connType == "802-11-wireless" && a.manager.WasRecentlyFailed(ssid) {
|
||||||
reason = "wrong-password"
|
reason = "wrong-password"
|
||||||
}
|
}
|
||||||
|
if settingName == "vpn" && isPKCS11Auth(conn, vpnSvc) {
|
||||||
|
reason = "pkcs11"
|
||||||
|
}
|
||||||
|
|
||||||
var connId, connUuid string
|
var connId, connUuid string
|
||||||
if c, ok := conn["connection"]; ok {
|
if c, ok := conn["connection"]; ok {
|
||||||
@@ -249,6 +252,28 @@ func (a *SecretAgent) GetSecrets(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if settingName == "vpn" && a.backend != nil {
|
if settingName == "vpn" && a.backend != nil {
|
||||||
|
// Check for cached PKCS11 PIN first
|
||||||
|
isPKCS11Request := len(fields) == 1 && fields[0] == "key_pass"
|
||||||
|
if isPKCS11Request {
|
||||||
|
a.backend.cachedPKCS11Mu.Lock()
|
||||||
|
cached := a.backend.cachedPKCS11PIN
|
||||||
|
if cached != nil && cached.ConnectionUUID == connUuid {
|
||||||
|
a.backend.cachedPKCS11PIN = nil
|
||||||
|
a.backend.cachedPKCS11Mu.Unlock()
|
||||||
|
|
||||||
|
log.Infof("[SecretAgent] Using cached PKCS11 PIN")
|
||||||
|
|
||||||
|
out := nmSettingMap{}
|
||||||
|
vpnSec := nmVariantMap{}
|
||||||
|
vpnSec["secrets"] = dbus.MakeVariant(map[string]string{"key_pass": cached.PIN})
|
||||||
|
out[settingName] = vpnSec
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
a.backend.cachedPKCS11Mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for cached VPN password
|
||||||
a.backend.cachedVPNCredsMu.Lock()
|
a.backend.cachedVPNCredsMu.Lock()
|
||||||
cached := a.backend.cachedVPNCreds
|
cached := a.backend.cachedVPNCreds
|
||||||
if cached != nil && cached.ConnectionUUID == connUuid {
|
if cached != nil && cached.ConnectionUUID == connUuid {
|
||||||
@@ -258,9 +283,9 @@ func (a *SecretAgent) GetSecrets(
|
|||||||
log.Infof("[SecretAgent] Using cached password from pre-activation prompt")
|
log.Infof("[SecretAgent] Using cached password from pre-activation prompt")
|
||||||
|
|
||||||
out := nmSettingMap{}
|
out := nmSettingMap{}
|
||||||
sec := nmVariantMap{}
|
vpnSec := nmVariantMap{}
|
||||||
sec["password"] = dbus.MakeVariant(cached.Password)
|
vpnSec["secrets"] = dbus.MakeVariant(map[string]string{"password": cached.Password})
|
||||||
out[settingName] = sec
|
out[settingName] = vpnSec
|
||||||
|
|
||||||
if cached.SavePassword {
|
if cached.SavePassword {
|
||||||
a.backend.pendingVPNSaveMu.Lock()
|
a.backend.pendingVPNSaveMu.Lock()
|
||||||
@@ -364,16 +389,41 @@ func (a *SecretAgent) GetSecrets(
|
|||||||
}
|
}
|
||||||
sec[k] = dbus.MakeVariant(v)
|
sec[k] = dbus.MakeVariant(v)
|
||||||
}
|
}
|
||||||
out[settingName] = sec
|
|
||||||
|
// Check if this is PKCS11 auth (key_pass)
|
||||||
|
pin, isPKCS11 := reply.Secrets["key_pass"]
|
||||||
|
|
||||||
switch settingName {
|
switch settingName {
|
||||||
case "802-1x":
|
|
||||||
log.Infof("[SecretAgent] Returning 802-1x enterprise secrets with %d fields", len(sec))
|
|
||||||
case "vpn":
|
case "vpn":
|
||||||
log.Infof("[SecretAgent] Returning VPN secrets with %d fields for %s", len(sec), vpnSvc)
|
// VPN secrets must be wrapped in a "secrets" key per NM spec
|
||||||
}
|
secretsDict := make(map[string]string)
|
||||||
|
for k, v := range reply.Secrets {
|
||||||
|
if k != "username" {
|
||||||
|
secretsDict[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vpnSec := nmVariantMap{}
|
||||||
|
vpnSec["secrets"] = dbus.MakeVariant(secretsDict)
|
||||||
|
out[settingName] = vpnSec
|
||||||
|
log.Infof("[SecretAgent] Returning VPN secrets with %d fields for %s", len(secretsDict), vpnSvc)
|
||||||
|
|
||||||
if settingName == "vpn" && a.backend != nil && (vpnUsername != "" || reply.Save) {
|
// Cache PKCS11 PIN in case GetSecrets is called again during activation
|
||||||
|
if isPKCS11 && a.backend != nil {
|
||||||
|
a.backend.cachedPKCS11Mu.Lock()
|
||||||
|
a.backend.cachedPKCS11PIN = &cachedPKCS11PIN{
|
||||||
|
ConnectionUUID: connUuid,
|
||||||
|
PIN: pin,
|
||||||
|
}
|
||||||
|
a.backend.cachedPKCS11Mu.Unlock()
|
||||||
|
log.Infof("[SecretAgent] Cached PKCS11 PIN for potential re-request")
|
||||||
|
}
|
||||||
|
case "802-1x":
|
||||||
|
out[settingName] = sec
|
||||||
|
log.Infof("[SecretAgent] Returning 802-1x enterprise secrets with %d fields", len(sec))
|
||||||
|
default:
|
||||||
|
out[settingName] = sec
|
||||||
|
}
|
||||||
|
if settingName == "vpn" && a.backend != nil && !isPKCS11 && (vpnUsername != "" || reply.Save) {
|
||||||
pw := reply.Secrets["password"]
|
pw := reply.Secrets["password"]
|
||||||
a.backend.pendingVPNSaveMu.Lock()
|
a.backend.pendingVPNSaveMu.Lock()
|
||||||
a.backend.pendingVPNSave = &pendingVPNCredentials{
|
a.backend.pendingVPNSave = &pendingVPNCredentials{
|
||||||
@@ -579,6 +629,15 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string {
|
|||||||
connType := dataMap["connection-type"]
|
connType := dataMap["connection-type"]
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
case strings.Contains(vpnService, "openconnect"):
|
||||||
|
authType := dataMap["authtype"]
|
||||||
|
userCert := dataMap["usercert"]
|
||||||
|
if authType == "cert" && strings.HasPrefix(userCert, "pkcs11:") {
|
||||||
|
return []string{"key_pass"}
|
||||||
|
}
|
||||||
|
if dataMap["username"] == "" {
|
||||||
|
fields = []string{"username", "password"}
|
||||||
|
}
|
||||||
case strings.Contains(vpnService, "openvpn"):
|
case strings.Contains(vpnService, "openvpn"):
|
||||||
if connType == "password" || connType == "password-tls" {
|
if connType == "password" || connType == "password-tls" {
|
||||||
if dataMap["username"] == "" {
|
if dataMap["username"] == "" {
|
||||||
@@ -586,7 +645,7 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case strings.Contains(vpnService, "vpnc"), strings.Contains(vpnService, "l2tp"),
|
case strings.Contains(vpnService, "vpnc"), strings.Contains(vpnService, "l2tp"),
|
||||||
strings.Contains(vpnService, "pptp"), strings.Contains(vpnService, "openconnect"):
|
strings.Contains(vpnService, "pptp"):
|
||||||
if dataMap["username"] == "" {
|
if dataMap["username"] == "" {
|
||||||
fields = []string{"username", "password"}
|
fields = []string{"username", "password"}
|
||||||
}
|
}
|
||||||
@@ -597,6 +656,8 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string {
|
|||||||
|
|
||||||
func vpnFieldMeta(field, vpnService string) (label string, isSecret bool) {
|
func vpnFieldMeta(field, vpnService string) (label string, isSecret bool) {
|
||||||
switch field {
|
switch field {
|
||||||
|
case "key_pass":
|
||||||
|
return "PIN", true
|
||||||
case "password":
|
case "password":
|
||||||
return "Password", true
|
return "Password", true
|
||||||
case "Xauth password":
|
case "Xauth password":
|
||||||
@@ -624,6 +685,25 @@ func vpnFieldMeta(field, vpnService string) (label string, isSecret bool) {
|
|||||||
return titleCaser.String(strings.ReplaceAll(field, "-", " ")), false
|
return titleCaser.String(strings.ReplaceAll(field, "-", " ")), false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isPKCS11Auth(conn map[string]nmVariantMap, vpnService string) bool {
|
||||||
|
if !strings.Contains(vpnService, "openconnect") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
vpnSettings, ok := conn["vpn"]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
dataVariant, ok := vpnSettings["data"]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
dataMap, ok := dataVariant.Value().(map[string]string)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return dataMap["authtype"] == "cert" && strings.HasPrefix(dataMap["usercert"], "pkcs11:")
|
||||||
|
}
|
||||||
|
|
||||||
func readVPNPasswordFlags(conn map[string]nmVariantMap, settingName string) uint32 {
|
func readVPNPasswordFlags(conn map[string]nmVariantMap, settingName string) uint32 {
|
||||||
if settingName != "vpn" {
|
if settingName != "vpn" {
|
||||||
return 0xFFFF
|
return 0xFFFF
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ type NetworkManagerBackend struct {
|
|||||||
pendingVPNSaveMu sync.Mutex
|
pendingVPNSaveMu sync.Mutex
|
||||||
cachedVPNCreds *cachedVPNCredentials
|
cachedVPNCreds *cachedVPNCredentials
|
||||||
cachedVPNCredsMu sync.Mutex
|
cachedVPNCredsMu sync.Mutex
|
||||||
|
cachedPKCS11PIN *cachedPKCS11PIN
|
||||||
|
cachedPKCS11Mu sync.Mutex
|
||||||
|
|
||||||
onStateChange func()
|
onStateChange func()
|
||||||
}
|
}
|
||||||
@@ -89,6 +91,11 @@ type cachedVPNCredentials struct {
|
|||||||
SavePassword bool
|
SavePassword bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type cachedPKCS11PIN struct {
|
||||||
|
ConnectionUUID string
|
||||||
|
PIN string
|
||||||
|
}
|
||||||
|
|
||||||
func NewNetworkManagerBackend(nmConn ...gonetworkmanager.NetworkManager) (*NetworkManagerBackend, error) {
|
func NewNetworkManagerBackend(nmConn ...gonetworkmanager.NetworkManager) (*NetworkManagerBackend, error) {
|
||||||
var nm gonetworkmanager.NetworkManager
|
var nm gonetworkmanager.NetworkManager
|
||||||
var err error
|
var err error
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func (b *NetworkManagerBackend) ListVPNProfiles() ([]VPNProfile, error) {
|
|||||||
return nil, fmt.Errorf("failed to get connections: %w", err)
|
return nil, fmt.Errorf("failed to get connections: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var profiles []VPNProfile
|
profiles := []VPNProfile{}
|
||||||
for _, conn := range connections {
|
for _, conn := range connections {
|
||||||
settings, err := conn.GetSettings()
|
settings, err := conn.GetSettings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -101,7 +101,7 @@ func (b *NetworkManagerBackend) ListActiveVPN() ([]VPNActive, error) {
|
|||||||
return nil, fmt.Errorf("failed to get active connections: %w", err)
|
return nil, fmt.Errorf("failed to get active connections: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var active []VPNActive
|
active := []VPNActive{}
|
||||||
for _, activeConn := range activeConns {
|
for _, activeConn := range activeConns {
|
||||||
connType, err := activeConn.GetPropertyType()
|
connType, err := activeConn.GetPropertyType()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -282,111 +282,26 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
needsUsernamePrePrompt := false
|
|
||||||
var vpnServiceType string
|
var vpnServiceType string
|
||||||
|
var vpnData map[string]string
|
||||||
if vpnSettings, ok := targetSettings["vpn"]; ok {
|
if vpnSettings, ok := targetSettings["vpn"]; ok {
|
||||||
if svc, ok := vpnSettings["service-type"].(string); ok {
|
if svc, ok := vpnSettings["service-type"].(string); ok {
|
||||||
vpnServiceType = svc
|
vpnServiceType = svc
|
||||||
}
|
}
|
||||||
if data, ok := vpnSettings["data"].(map[string]string); ok {
|
if data, ok := vpnSettings["data"].(map[string]string); ok {
|
||||||
connType := data["connection-type"]
|
vpnData = data
|
||||||
username := data["username"]
|
|
||||||
// OpenVPN password auth needs username in vpn.data
|
|
||||||
if strings.Contains(vpnServiceType, "openvpn") &&
|
|
||||||
(connType == "password" || connType == "password-tls") &&
|
|
||||||
username == "" {
|
|
||||||
needsUsernamePrePrompt = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If username is needed but missing, prompt for it before activating
|
authAction := detectVPNAuthAction(vpnServiceType, vpnData)
|
||||||
if needsUsernamePrePrompt && b.promptBroker != nil {
|
|
||||||
log.Infof("[ConnectVPN] OpenVPN requires username in vpn.data - prompting before activation")
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
switch authAction {
|
||||||
defer cancel()
|
case "openvpn_username":
|
||||||
|
if b.promptBroker == nil {
|
||||||
token, err := b.promptBroker.Ask(ctx, PromptRequest{
|
return fmt.Errorf("OpenVPN password authentication requires interactive prompt")
|
||||||
Name: connName,
|
|
||||||
ConnType: "vpn",
|
|
||||||
VpnService: vpnServiceType,
|
|
||||||
SettingName: "vpn",
|
|
||||||
Fields: []string{"username", "password"},
|
|
||||||
FieldsInfo: []FieldInfo{{Name: "username", Label: "Username", IsSecret: false}, {Name: "password", Label: "Password", IsSecret: true}},
|
|
||||||
Reason: "required",
|
|
||||||
ConnectionId: connName,
|
|
||||||
ConnectionUuid: targetUUID,
|
|
||||||
ConnectionPath: string(targetConn.GetPath()),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to request credentials: %w", err)
|
|
||||||
}
|
}
|
||||||
|
if err := b.handleOpenVPNUsernameAuth(targetConn, connName, targetUUID, vpnServiceType); err != nil {
|
||||||
reply, err := b.promptBroker.Wait(ctx, token)
|
return err
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("credentials prompt failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
username := reply.Secrets["username"]
|
|
||||||
password := reply.Secrets["password"]
|
|
||||||
if username != "" {
|
|
||||||
connObj := b.dbusConn.Object("org.freedesktop.NetworkManager", targetConn.GetPath())
|
|
||||||
var existingSettings map[string]map[string]dbus.Variant
|
|
||||||
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&existingSettings); err != nil {
|
|
||||||
return fmt.Errorf("failed to get settings for username save: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
settings := make(map[string]map[string]dbus.Variant)
|
|
||||||
if connSection, ok := existingSettings["connection"]; ok {
|
|
||||||
settings["connection"] = connSection
|
|
||||||
}
|
|
||||||
vpn := existingSettings["vpn"]
|
|
||||||
var data map[string]string
|
|
||||||
if dataVariant, ok := vpn["data"]; ok {
|
|
||||||
if dm, ok := dataVariant.Value().(map[string]string); ok {
|
|
||||||
data = make(map[string]string)
|
|
||||||
for k, v := range dm {
|
|
||||||
data[k] = v
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
data = make(map[string]string)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
data = make(map[string]string)
|
|
||||||
}
|
|
||||||
data["username"] = username
|
|
||||||
|
|
||||||
if reply.Save && password != "" {
|
|
||||||
data["password-flags"] = "0"
|
|
||||||
secs := make(map[string]string)
|
|
||||||
secs["password"] = password
|
|
||||||
vpn["secrets"] = dbus.MakeVariant(secs)
|
|
||||||
log.Infof("[ConnectVPN] Saving username and password to vpn.data")
|
|
||||||
} else {
|
|
||||||
log.Infof("[ConnectVPN] Saving username to vpn.data (password will be prompted)")
|
|
||||||
}
|
|
||||||
|
|
||||||
vpn["data"] = dbus.MakeVariant(data)
|
|
||||||
settings["vpn"] = vpn
|
|
||||||
|
|
||||||
var result map[string]dbus.Variant
|
|
||||||
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.Update2", 0,
|
|
||||||
settings, uint32(0x1), map[string]dbus.Variant{}).Store(&result); err != nil {
|
|
||||||
return fmt.Errorf("failed to save username: %w", err)
|
|
||||||
}
|
|
||||||
log.Infof("[ConnectVPN] Username saved to connection, now activating")
|
|
||||||
|
|
||||||
if password != "" && !reply.Save {
|
|
||||||
b.cachedVPNCredsMu.Lock()
|
|
||||||
b.cachedVPNCreds = &cachedVPNCredentials{
|
|
||||||
ConnectionUUID: targetUUID,
|
|
||||||
Password: password,
|
|
||||||
SavePassword: reply.Save,
|
|
||||||
}
|
|
||||||
b.cachedVPNCredsMu.Unlock()
|
|
||||||
log.Infof("[ConnectVPN] Cached password for GetSecrets")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,6 +332,119 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func detectVPNAuthAction(serviceType string, data map[string]string) string {
|
||||||
|
if data == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.Contains(serviceType, "openvpn"):
|
||||||
|
connType := data["connection-type"]
|
||||||
|
username := data["username"]
|
||||||
|
if (connType == "password" || connType == "password-tls") && username == "" {
|
||||||
|
return "openvpn_username"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *NetworkManagerBackend) handleOpenVPNUsernameAuth(targetConn gonetworkmanager.Connection, connName, targetUUID, vpnServiceType string) error {
|
||||||
|
log.Infof("[ConnectVPN] OpenVPN requires username in vpn.data - prompting before activation")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
token, err := b.promptBroker.Ask(ctx, PromptRequest{
|
||||||
|
Name: connName,
|
||||||
|
ConnType: "vpn",
|
||||||
|
VpnService: vpnServiceType,
|
||||||
|
SettingName: "vpn",
|
||||||
|
Fields: []string{"username", "password"},
|
||||||
|
FieldsInfo: []FieldInfo{{Name: "username", Label: "Username", IsSecret: false}, {Name: "password", Label: "Password", IsSecret: true}},
|
||||||
|
Reason: "required",
|
||||||
|
ConnectionId: connName,
|
||||||
|
ConnectionUuid: targetUUID,
|
||||||
|
ConnectionPath: string(targetConn.GetPath()),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to request credentials: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reply, err := b.promptBroker.Wait(ctx, token)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("credentials prompt failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if reply.Cancel {
|
||||||
|
return fmt.Errorf("user cancelled authentication")
|
||||||
|
}
|
||||||
|
|
||||||
|
username := reply.Secrets["username"]
|
||||||
|
password := reply.Secrets["password"]
|
||||||
|
if username == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
connObj := b.dbusConn.Object("org.freedesktop.NetworkManager", targetConn.GetPath())
|
||||||
|
var existingSettings map[string]map[string]dbus.Variant
|
||||||
|
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&existingSettings); err != nil {
|
||||||
|
return fmt.Errorf("failed to get settings for username save: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
settings := make(map[string]map[string]dbus.Variant)
|
||||||
|
if connSection, ok := existingSettings["connection"]; ok {
|
||||||
|
settings["connection"] = connSection
|
||||||
|
}
|
||||||
|
vpn := existingSettings["vpn"]
|
||||||
|
var data map[string]string
|
||||||
|
if dataVariant, ok := vpn["data"]; ok {
|
||||||
|
if dm, ok := dataVariant.Value().(map[string]string); ok {
|
||||||
|
data = make(map[string]string)
|
||||||
|
for k, v := range dm {
|
||||||
|
data[k] = v
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data = make(map[string]string)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data = make(map[string]string)
|
||||||
|
}
|
||||||
|
data["username"] = username
|
||||||
|
|
||||||
|
if reply.Save && password != "" {
|
||||||
|
data["password-flags"] = "0"
|
||||||
|
secs := make(map[string]string)
|
||||||
|
secs["password"] = password
|
||||||
|
vpn["secrets"] = dbus.MakeVariant(secs)
|
||||||
|
log.Infof("[ConnectVPN] Saving username and password to vpn.data")
|
||||||
|
} else {
|
||||||
|
log.Infof("[ConnectVPN] Saving username to vpn.data (password will be prompted)")
|
||||||
|
}
|
||||||
|
|
||||||
|
vpn["data"] = dbus.MakeVariant(data)
|
||||||
|
settings["vpn"] = vpn
|
||||||
|
|
||||||
|
var result map[string]dbus.Variant
|
||||||
|
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.Update2", 0,
|
||||||
|
settings, uint32(0x1), map[string]dbus.Variant{}).Store(&result); err != nil {
|
||||||
|
return fmt.Errorf("failed to save username: %w", err)
|
||||||
|
}
|
||||||
|
log.Infof("[ConnectVPN] Username saved to connection")
|
||||||
|
|
||||||
|
if password != "" && !reply.Save {
|
||||||
|
b.cachedVPNCredsMu.Lock()
|
||||||
|
b.cachedVPNCreds = &cachedVPNCredentials{
|
||||||
|
ConnectionUUID: targetUUID,
|
||||||
|
Password: password,
|
||||||
|
SavePassword: reply.Save,
|
||||||
|
}
|
||||||
|
b.cachedVPNCredsMu.Unlock()
|
||||||
|
log.Infof("[ConnectVPN] Cached password for GetSecrets")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (b *NetworkManagerBackend) DisconnectVPN(uuidOrName string) error {
|
func (b *NetworkManagerBackend) DisconnectVPN(uuidOrName string) error {
|
||||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
||||||
|
|
||||||
@@ -655,6 +683,11 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
|
|||||||
b.state.LastError = ""
|
b.state.LastError = ""
|
||||||
b.stateMutex.Unlock()
|
b.stateMutex.Unlock()
|
||||||
|
|
||||||
|
// Clear cached PKCS11 PIN on success
|
||||||
|
b.cachedPKCS11Mu.Lock()
|
||||||
|
b.cachedPKCS11PIN = nil
|
||||||
|
b.cachedPKCS11Mu.Unlock()
|
||||||
|
|
||||||
b.pendingVPNSaveMu.Lock()
|
b.pendingVPNSaveMu.Lock()
|
||||||
pending := b.pendingVPNSave
|
pending := b.pendingVPNSave
|
||||||
b.pendingVPNSave = nil
|
b.pendingVPNSave = nil
|
||||||
@@ -671,6 +704,11 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
|
|||||||
b.state.ConnectingVPNUUID = ""
|
b.state.ConnectingVPNUUID = ""
|
||||||
b.state.LastError = "VPN connection failed"
|
b.state.LastError = "VPN connection failed"
|
||||||
b.stateMutex.Unlock()
|
b.stateMutex.Unlock()
|
||||||
|
|
||||||
|
// Clear cached PKCS11 PIN on failure
|
||||||
|
b.cachedPKCS11Mu.Lock()
|
||||||
|
b.cachedPKCS11PIN = nil
|
||||||
|
b.cachedPKCS11Mu.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -683,6 +721,11 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
|
|||||||
b.state.ConnectingVPNUUID = ""
|
b.state.ConnectingVPNUUID = ""
|
||||||
b.state.LastError = "VPN connection failed"
|
b.state.LastError = "VPN connection failed"
|
||||||
b.stateMutex.Unlock()
|
b.stateMutex.Unlock()
|
||||||
|
|
||||||
|
// Clear cached PKCS11 PIN
|
||||||
|
b.cachedPKCS11Mu.Lock()
|
||||||
|
b.cachedPKCS11PIN = nil
|
||||||
|
b.cachedPKCS11Mu.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ func handleConnectWiFi(conn net.Conn, req models.Request, manager *Manager) {
|
|||||||
connReq.Username = params.StringOpt(req.Params, "username", "")
|
connReq.Username = params.StringOpt(req.Params, "username", "")
|
||||||
connReq.Device = params.StringOpt(req.Params, "device", "")
|
connReq.Device = params.StringOpt(req.Params, "device", "")
|
||||||
|
|
||||||
if interactive, ok := req.Params["interactive"].(bool); ok {
|
if interactive, ok := models.Get[bool](req, "interactive"); ok {
|
||||||
connReq.Interactive = interactive
|
connReq.Interactive = interactive
|
||||||
} else {
|
} else {
|
||||||
state := manager.GetState()
|
state := manager.GetState()
|
||||||
@@ -185,7 +185,7 @@ func handleConnectWiFi(conn net.Conn, req models.Request, manager *Manager) {
|
|||||||
connReq.ClientCertPath = params.StringOpt(req.Params, "clientCertPath", "")
|
connReq.ClientCertPath = params.StringOpt(req.Params, "clientCertPath", "")
|
||||||
connReq.PrivateKeyPath = params.StringOpt(req.Params, "privateKeyPath", "")
|
connReq.PrivateKeyPath = params.StringOpt(req.Params, "privateKeyPath", "")
|
||||||
|
|
||||||
if useSystemCACerts, ok := req.Params["useSystemCACerts"].(bool); ok {
|
if useSystemCACerts, ok := models.Get[bool](req, "useSystemCACerts"); ok {
|
||||||
connReq.UseSystemCACerts = &useSystemCACerts
|
connReq.UseSystemCACerts = &useSystemCACerts
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,13 +528,13 @@ func handleUpdateVPNConfig(conn net.Conn, req models.Request, manager *Manager)
|
|||||||
|
|
||||||
updates := make(map[string]any)
|
updates := make(map[string]any)
|
||||||
|
|
||||||
if name, ok := req.Params["name"].(string); ok {
|
if name, ok := models.Get[string](req, "name"); ok {
|
||||||
updates["name"] = name
|
updates["name"] = name
|
||||||
}
|
}
|
||||||
if autoconnect, ok := req.Params["autoconnect"].(bool); ok {
|
if autoconnect, ok := models.Get[bool](req, "autoconnect"); ok {
|
||||||
updates["autoconnect"] = autoconnect
|
updates["autoconnect"] = autoconnect
|
||||||
}
|
}
|
||||||
if data, ok := req.Params["data"].(map[string]any); ok {
|
if data, ok := models.Get[map[string]any](req, "data"); ok {
|
||||||
updates["data"] = data
|
updates["data"] = data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func HandleInstall(conn net.Conn, req models.Request) {
|
func HandleInstall(conn net.Conn, req models.Request) {
|
||||||
idOrName, ok := req.Params["name"].(string)
|
idOrName, ok := models.Get[string](req, "name")
|
||||||
if !ok {
|
if !ok {
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
|
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func HandleSearch(conn net.Conn, req models.Request) {
|
func HandleSearch(conn net.Conn, req models.Request) {
|
||||||
query, ok := req.Params["query"].(string)
|
query, ok := models.Get[string](req, "query")
|
||||||
if !ok {
|
if !ok {
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'query' parameter")
|
models.RespondError(conn, req.ID, "missing or invalid 'query' parameter")
|
||||||
return
|
return
|
||||||
@@ -30,15 +30,15 @@ func HandleSearch(conn net.Conn, req models.Request) {
|
|||||||
|
|
||||||
searchResults := plugins.FuzzySearch(query, pluginList)
|
searchResults := plugins.FuzzySearch(query, pluginList)
|
||||||
|
|
||||||
if category, ok := req.Params["category"].(string); ok && category != "" {
|
if category := models.GetOr(req, "category", ""); category != "" {
|
||||||
searchResults = plugins.FilterByCategory(category, searchResults)
|
searchResults = plugins.FilterByCategory(category, searchResults)
|
||||||
}
|
}
|
||||||
|
|
||||||
if compositor, ok := req.Params["compositor"].(string); ok && compositor != "" {
|
if compositor := models.GetOr(req, "compositor", ""); compositor != "" {
|
||||||
searchResults = plugins.FilterByCompositor(compositor, searchResults)
|
searchResults = plugins.FilterByCompositor(compositor, searchResults)
|
||||||
}
|
}
|
||||||
|
|
||||||
if capability, ok := req.Params["capability"].(string); ok && capability != "" {
|
if capability := models.GetOr(req, "capability", ""); capability != "" {
|
||||||
searchResults = plugins.FilterByCapability(capability, searchResults)
|
searchResults = plugins.FilterByCapability(capability, searchResults)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func HandleUninstall(conn net.Conn, req models.Request) {
|
func HandleUninstall(conn net.Conn, req models.Request) {
|
||||||
name, ok := req.Params["name"].(string)
|
name, ok := models.Get[string](req, "name")
|
||||||
if !ok {
|
if !ok {
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
|
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func HandleUpdate(conn net.Conn, req models.Request) {
|
func HandleUpdate(conn net.Conn, req models.Request) {
|
||||||
name, ok := req.Params["name"].(string)
|
name, ok := models.Get[string](req, "name")
|
||||||
if !ok {
|
if !ok {
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
|
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
||||||
serverPlugins "github.com/AvengeMedia/DankMaterialShell/core/internal/server/plugins"
|
serverPlugins "github.com/AvengeMedia/DankMaterialShell/core/internal/server/plugins"
|
||||||
|
serverThemes "github.com/AvengeMedia/DankMaterialShell/core/internal/server/themes"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
|
||||||
)
|
)
|
||||||
@@ -37,6 +38,11 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(req.Method, "themes.") {
|
||||||
|
serverThemes.HandleRequest(conn, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(req.Method, "loginctl.") {
|
if strings.HasPrefix(req.Method, "loginctl.") {
|
||||||
if loginctlManager == nil {
|
if loginctlManager == nil {
|
||||||
models.RespondError(conn, req.ID, "loginctl manager not initialized")
|
models.RespondError(conn, req.ID, "loginctl manager not initialized")
|
||||||
@@ -186,27 +192,21 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
|||||||
func handleClipboardSetConfig(conn net.Conn, req models.Request) {
|
func handleClipboardSetConfig(conn net.Conn, req models.Request) {
|
||||||
cfg := clipboard.LoadConfig()
|
cfg := clipboard.LoadConfig()
|
||||||
|
|
||||||
if v, ok := req.Params["maxHistory"].(float64); ok {
|
if v, ok := models.Get[float64](req, "maxHistory"); ok {
|
||||||
cfg.MaxHistory = int(v)
|
cfg.MaxHistory = int(v)
|
||||||
}
|
}
|
||||||
if v, ok := req.Params["maxEntrySize"].(float64); ok {
|
if v, ok := models.Get[float64](req, "maxEntrySize"); ok {
|
||||||
cfg.MaxEntrySize = int64(v)
|
cfg.MaxEntrySize = int64(v)
|
||||||
}
|
}
|
||||||
if v, ok := req.Params["autoClearDays"].(float64); ok {
|
if v, ok := models.Get[float64](req, "autoClearDays"); ok {
|
||||||
cfg.AutoClearDays = int(v)
|
cfg.AutoClearDays = int(v)
|
||||||
}
|
}
|
||||||
if v, ok := req.Params["clearAtStartup"].(bool); ok {
|
if v, ok := models.Get[bool](req, "clearAtStartup"); ok {
|
||||||
cfg.ClearAtStartup = v
|
cfg.ClearAtStartup = v
|
||||||
}
|
}
|
||||||
if v, ok := req.Params["disabled"].(bool); ok {
|
if v, ok := models.Get[bool](req, "disabled"); ok {
|
||||||
cfg.Disabled = v
|
cfg.Disabled = v
|
||||||
}
|
}
|
||||||
if v, ok := req.Params["disableHistory"].(bool); ok {
|
|
||||||
cfg.DisableHistory = v
|
|
||||||
}
|
|
||||||
if v, ok := req.Params["disablePersist"].(bool); ok {
|
|
||||||
cfg.DisablePersist = v
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := clipboard.SaveConfig(cfg); err != nil {
|
if err := clipboard.SaveConfig(cfg); err != nil {
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
|||||||
@@ -520,7 +520,7 @@ func handleSubscribe(conn net.Conn, req models.Request) {
|
|||||||
clientID := fmt.Sprintf("meta-client-%p", conn)
|
clientID := fmt.Sprintf("meta-client-%p", conn)
|
||||||
|
|
||||||
var services []string
|
var services []string
|
||||||
if servicesParam, ok := req.Params["services"].([]any); ok {
|
if servicesParam, ok := models.Get[[]any](req, "services"); ok {
|
||||||
for _, s := range servicesParam {
|
for _, s := range servicesParam {
|
||||||
if str, ok := s.(string); ok {
|
if str, ok := s.(string); ok {
|
||||||
services = append(services, str)
|
services = append(services, str)
|
||||||
|
|||||||
27
core/internal/server/themes/handlers.go
Normal file
27
core/internal/server/themes/handlers.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package themes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleRequest(conn net.Conn, req models.Request) {
|
||||||
|
switch req.Method {
|
||||||
|
case "themes.list":
|
||||||
|
HandleList(conn, req)
|
||||||
|
case "themes.listInstalled":
|
||||||
|
HandleListInstalled(conn, req)
|
||||||
|
case "themes.install":
|
||||||
|
HandleInstall(conn, req)
|
||||||
|
case "themes.uninstall":
|
||||||
|
HandleUninstall(conn, req)
|
||||||
|
case "themes.update":
|
||||||
|
HandleUpdate(conn, req)
|
||||||
|
case "themes.search":
|
||||||
|
HandleSearch(conn, req)
|
||||||
|
default:
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
||||||
|
}
|
||||||
|
}
|
||||||
52
core/internal/server/themes/install.go
Normal file
52
core/internal/server/themes/install.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package themes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/themes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleInstall(conn net.Conn, req models.Request) {
|
||||||
|
idOrName, ok := models.Get[string](req, "name")
|
||||||
|
if !ok {
|
||||||
|
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
registry, err := themes.NewRegistry()
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
themeList, err := registry.List()
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("failed to list themes: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
theme := themes.FindByIDOrName(idOrName, themeList)
|
||||||
|
if theme == nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("theme not found: %s", idOrName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manager, err := themes.NewManager()
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
registryThemeDir := registry.GetThemeDir(theme.SourceDir)
|
||||||
|
if err := manager.Install(*theme, registryThemeDir); err != nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("failed to install theme: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{
|
||||||
|
Success: true,
|
||||||
|
Message: fmt.Sprintf("theme installed: %s", theme.Name),
|
||||||
|
})
|
||||||
|
}
|
||||||
54
core/internal/server/themes/list.go
Normal file
54
core/internal/server/themes/list.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package themes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/themes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleList(conn net.Conn, req models.Request) {
|
||||||
|
registry, err := themes.NewRegistry()
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
themeList, err := registry.List()
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("failed to list themes: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manager, err := themes.NewManager()
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]ThemeInfo, len(themeList))
|
||||||
|
for i, t := range themeList {
|
||||||
|
installed, _ := manager.IsInstalled(t)
|
||||||
|
info := ThemeInfo{
|
||||||
|
ID: t.ID,
|
||||||
|
Name: t.Name,
|
||||||
|
Version: t.Version,
|
||||||
|
Author: t.Author,
|
||||||
|
Description: t.Description,
|
||||||
|
PreviewPath: t.PreviewPath,
|
||||||
|
SourceDir: t.SourceDir,
|
||||||
|
Installed: installed,
|
||||||
|
FirstParty: isFirstParty(t.Author),
|
||||||
|
}
|
||||||
|
addVariantsInfo(&info, t.Variants)
|
||||||
|
result[i] = info
|
||||||
|
}
|
||||||
|
|
||||||
|
models.Respond(conn, req.ID, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isFirstParty(author string) bool {
|
||||||
|
return strings.EqualFold(author, "Avenge Media") || strings.EqualFold(author, "AvengeMedia")
|
||||||
|
}
|
||||||
157
core/internal/server/themes/list_installed.go
Normal file
157
core/internal/server/themes/list_installed.go
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
package themes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/themes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addVariantsInfo(info *ThemeInfo, variants *themes.ThemeVariants) {
|
||||||
|
if variants == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if variants.Type == "multi" {
|
||||||
|
if len(variants.Flavors) == 0 && len(variants.Accents) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
info.HasVariants = true
|
||||||
|
info.Variants = &VariantsInfo{
|
||||||
|
Type: "multi",
|
||||||
|
Flavors: make([]FlavorInfo, len(variants.Flavors)),
|
||||||
|
Accents: make([]AccentInfo, len(variants.Accents)),
|
||||||
|
}
|
||||||
|
if variants.Defaults != nil {
|
||||||
|
info.Variants.Defaults = &MultiDefaults{
|
||||||
|
Dark: variants.Defaults.Dark,
|
||||||
|
Light: variants.Defaults.Light,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, f := range variants.Flavors {
|
||||||
|
mode := ""
|
||||||
|
switch {
|
||||||
|
case f.Dark.Primary != "" && f.Light.Primary != "":
|
||||||
|
mode = "both"
|
||||||
|
case f.Dark.Primary != "":
|
||||||
|
mode = "dark"
|
||||||
|
case f.Light.Primary != "":
|
||||||
|
mode = "light"
|
||||||
|
default:
|
||||||
|
if f.Dark.Surface != "" {
|
||||||
|
mode = "dark"
|
||||||
|
} else if f.Light.Surface != "" {
|
||||||
|
mode = "light"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info.Variants.Flavors[i] = FlavorInfo{ID: f.ID, Name: f.Name, Mode: mode}
|
||||||
|
}
|
||||||
|
for i, a := range variants.Accents {
|
||||||
|
color := ""
|
||||||
|
if colors, ok := a.FlavorColors["mocha"]; ok && colors.Primary != "" {
|
||||||
|
color = colors.Primary
|
||||||
|
} else if colors, ok := a.FlavorColors["latte"]; ok && colors.Primary != "" {
|
||||||
|
color = colors.Primary
|
||||||
|
} else {
|
||||||
|
for _, c := range a.FlavorColors {
|
||||||
|
if c.Primary != "" {
|
||||||
|
color = c.Primary
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info.Variants.Accents[i] = AccentInfo{ID: a.ID, Name: a.Name, Color: color}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(variants.Options) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
info.HasVariants = true
|
||||||
|
info.Variants = &VariantsInfo{
|
||||||
|
Default: variants.Default,
|
||||||
|
Options: make([]VariantInfo, len(variants.Options)),
|
||||||
|
}
|
||||||
|
for i, v := range variants.Options {
|
||||||
|
info.Variants.Options[i] = VariantInfo{ID: v.ID, Name: v.Name}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleListInstalled(conn net.Conn, req models.Request) {
|
||||||
|
manager, err := themes.NewManager()
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
installedIDs, err := manager.ListInstalled()
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("failed to list installed themes: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
registry, err := themes.NewRegistry()
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
allThemes, err := registry.List()
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("failed to list themes: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
themeMap := make(map[string]themes.Theme)
|
||||||
|
for _, t := range allThemes {
|
||||||
|
themeMap[t.ID] = t
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]ThemeInfo, 0, len(installedIDs))
|
||||||
|
for _, id := range installedIDs {
|
||||||
|
if theme, ok := themeMap[id]; ok {
|
||||||
|
hasUpdate := false
|
||||||
|
if hasUpdates, err := manager.HasUpdates(id, theme); err == nil {
|
||||||
|
hasUpdate = hasUpdates
|
||||||
|
}
|
||||||
|
|
||||||
|
info := ThemeInfo{
|
||||||
|
ID: theme.ID,
|
||||||
|
Name: theme.Name,
|
||||||
|
Version: theme.Version,
|
||||||
|
Author: theme.Author,
|
||||||
|
Description: theme.Description,
|
||||||
|
SourceDir: id,
|
||||||
|
FirstParty: isFirstParty(theme.Author),
|
||||||
|
HasUpdate: hasUpdate,
|
||||||
|
}
|
||||||
|
addVariantsInfo(&info, theme.Variants)
|
||||||
|
result = append(result, info)
|
||||||
|
} else {
|
||||||
|
installed, err := manager.GetInstalledTheme(id)
|
||||||
|
if err != nil {
|
||||||
|
result = append(result, ThemeInfo{
|
||||||
|
ID: id,
|
||||||
|
Name: id,
|
||||||
|
SourceDir: id,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
info := ThemeInfo{
|
||||||
|
ID: installed.ID,
|
||||||
|
Name: installed.Name,
|
||||||
|
Version: installed.Version,
|
||||||
|
Author: installed.Author,
|
||||||
|
Description: installed.Description,
|
||||||
|
SourceDir: id,
|
||||||
|
FirstParty: isFirstParty(installed.Author),
|
||||||
|
}
|
||||||
|
addVariantsInfo(&info, installed.Variants)
|
||||||
|
result = append(result, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
models.Respond(conn, req.ID, result)
|
||||||
|
}
|
||||||
53
core/internal/server/themes/search.go
Normal file
53
core/internal/server/themes/search.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package themes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/themes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleSearch(conn net.Conn, req models.Request) {
|
||||||
|
query, ok := models.Get[string](req, "query")
|
||||||
|
if !ok {
|
||||||
|
models.RespondError(conn, req.ID, "missing or invalid 'query' parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
registry, err := themes.NewRegistry()
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
themeList, err := registry.List()
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("failed to list themes: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
searchResults := themes.FuzzySearch(query, themeList)
|
||||||
|
|
||||||
|
manager, err := themes.NewManager()
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]ThemeInfo, len(searchResults))
|
||||||
|
for i, t := range searchResults {
|
||||||
|
installed, _ := manager.IsInstalled(t)
|
||||||
|
result[i] = ThemeInfo{
|
||||||
|
ID: t.ID,
|
||||||
|
Name: t.Name,
|
||||||
|
Version: t.Version,
|
||||||
|
Author: t.Author,
|
||||||
|
Description: t.Description,
|
||||||
|
Installed: installed,
|
||||||
|
FirstParty: isFirstParty(t.Author),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
models.Respond(conn, req.ID, result)
|
||||||
|
}
|
||||||
47
core/internal/server/themes/types.go
Normal file
47
core/internal/server/themes/types.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package themes
|
||||||
|
|
||||||
|
type VariantInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FlavorInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Mode string `json:"mode,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccentInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Color string `json:"color,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MultiDefaults struct {
|
||||||
|
Dark map[string]string `json:"dark,omitempty"`
|
||||||
|
Light map[string]string `json:"light,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VariantsInfo struct {
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Default string `json:"default,omitempty"`
|
||||||
|
Defaults *MultiDefaults `json:"defaults,omitempty"`
|
||||||
|
Options []VariantInfo `json:"options,omitempty"`
|
||||||
|
Flavors []FlavorInfo `json:"flavors,omitempty"`
|
||||||
|
Accents []AccentInfo `json:"accents,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThemeInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Author string `json:"author,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
PreviewPath string `json:"previewPath,omitempty"`
|
||||||
|
SourceDir string `json:"sourceDir,omitempty"`
|
||||||
|
Installed bool `json:"installed,omitempty"`
|
||||||
|
FirstParty bool `json:"firstParty,omitempty"`
|
||||||
|
HasUpdate bool `json:"hasUpdate,omitempty"`
|
||||||
|
HasVariants bool `json:"hasVariants,omitempty"`
|
||||||
|
Variants *VariantsInfo `json:"variants,omitempty"`
|
||||||
|
}
|
||||||
63
core/internal/server/themes/uninstall.go
Normal file
63
core/internal/server/themes/uninstall.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package themes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/themes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleUninstall(conn net.Conn, req models.Request) {
|
||||||
|
idOrName, ok := models.Get[string](req, "name")
|
||||||
|
if !ok {
|
||||||
|
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manager, err := themes.NewManager()
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
registry, err := themes.NewRegistry()
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
themeList, _ := registry.List()
|
||||||
|
theme := themes.FindByIDOrName(idOrName, themeList)
|
||||||
|
|
||||||
|
if theme != nil {
|
||||||
|
installed, err := manager.IsInstalled(*theme)
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("failed to check if theme is installed: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !installed {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("theme not installed: %s", idOrName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := manager.Uninstall(*theme); err != nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("failed to uninstall theme: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{
|
||||||
|
Success: true,
|
||||||
|
Message: fmt.Sprintf("theme uninstalled: %s", theme.Name),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := manager.UninstallByID(idOrName); err != nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("theme not found: %s", idOrName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{
|
||||||
|
Success: true,
|
||||||
|
Message: fmt.Sprintf("theme uninstalled: %s", idOrName),
|
||||||
|
})
|
||||||
|
}
|
||||||
57
core/internal/server/themes/update.go
Normal file
57
core/internal/server/themes/update.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package themes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/themes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleUpdate(conn net.Conn, req models.Request) {
|
||||||
|
idOrName, ok := models.Get[string](req, "name")
|
||||||
|
if !ok {
|
||||||
|
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manager, err := themes.NewManager()
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
registry, err := themes.NewRegistry()
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
themeList, _ := registry.List()
|
||||||
|
theme := themes.FindByIDOrName(idOrName, themeList)
|
||||||
|
|
||||||
|
if theme == nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("theme not found in registry: %s", idOrName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
installed, err := manager.IsInstalled(*theme)
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("failed to check if theme is installed: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !installed {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("theme not installed: %s", idOrName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := manager.Update(*theme); err != nil {
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("failed to update theme: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{
|
||||||
|
Success: true,
|
||||||
|
Message: fmt.Sprintf("theme updated: %s", theme.Name),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@ func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
|
|||||||
func handleSetTemperature(conn net.Conn, req models.Request, manager *Manager) {
|
func handleSetTemperature(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
var lowTemp, highTemp int
|
var lowTemp, highTemp int
|
||||||
|
|
||||||
if temp, ok := req.Params["temp"].(float64); ok {
|
if temp, ok := models.Get[float64](req, "temp"); ok {
|
||||||
lowTemp = int(temp)
|
lowTemp = int(temp)
|
||||||
highTemp = int(temp)
|
highTemp = int(temp)
|
||||||
} else {
|
} else {
|
||||||
@@ -93,24 +93,10 @@ func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleSetManualTimes(conn net.Conn, req models.Request, manager *Manager) {
|
func handleSetManualTimes(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
sunriseParam := req.Params["sunrise"]
|
sunriseStr, sunriseOK := models.Get[string](req, "sunrise")
|
||||||
sunsetParam := req.Params["sunset"]
|
sunsetStr, sunsetOK := models.Get[string](req, "sunset")
|
||||||
|
|
||||||
if sunriseParam == nil || sunsetParam == nil {
|
if !sunriseOK || !sunsetOK || sunriseStr == "" || sunsetStr == "" {
|
||||||
manager.ClearManualTimes()
|
|
||||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "manual times cleared"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sunriseStr, ok := sunriseParam.(string)
|
|
||||||
if !ok || sunriseStr == "" {
|
|
||||||
manager.ClearManualTimes()
|
|
||||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "manual times cleared"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sunsetStr, ok := sunsetParam.(string)
|
|
||||||
if !ok || sunsetStr == "" {
|
|
||||||
manager.ClearManualTimes()
|
manager.ClearManualTimes()
|
||||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "manual times cleared"})
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "manual times cleared"})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -878,18 +878,35 @@ func (m *Manager) handleDBusSignal(sig *dbus.Signal) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
preparing, ok := sig.Body[0].(bool)
|
preparing, ok := sig.Body[0].(bool)
|
||||||
if !ok {
|
if !ok || preparing {
|
||||||
return
|
|
||||||
}
|
|
||||||
if preparing {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
m.configMutex.RLock()
|
m.configMutex.RLock()
|
||||||
enabled := m.config.Enabled
|
enabled := m.config.Enabled
|
||||||
m.configMutex.RUnlock()
|
m.configMutex.RUnlock()
|
||||||
if enabled {
|
if !enabled {
|
||||||
m.triggerUpdate()
|
return
|
||||||
}
|
}
|
||||||
|
time.AfterFunc(500*time.Millisecond, func() {
|
||||||
|
m.post(func() {
|
||||||
|
m.configMutex.RLock()
|
||||||
|
stillEnabled := m.config.Enabled
|
||||||
|
m.configMutex.RUnlock()
|
||||||
|
if !stillEnabled || !m.controlsInitialized {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.outputs.Range(func(_ uint32, out *outputState) bool {
|
||||||
|
if out.gammaControl != nil {
|
||||||
|
out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1).Destroy()
|
||||||
|
out.gammaControl = nil
|
||||||
|
}
|
||||||
|
out.retryCount = 0
|
||||||
|
out.failed = false
|
||||||
|
m.recreateOutputControl(out)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) triggerUpdate() {
|
func (m *Manager) triggerUpdate() {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
|
|
||||||
func TestManager_ActorSerializesOutputStateAccess(t *testing.T) {
|
func TestManager_ActorSerializesOutputStateAccess(t *testing.T) {
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
cmdq: make(chan cmd, 128),
|
cmdq: make(chan cmd, 8192),
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,29 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSharedContext_ConcurrentPostNonBlocking(t *testing.T) {
|
func newTestSharedContext(t *testing.T, queueSize int) *SharedContext {
|
||||||
sc := &SharedContext{
|
t.Helper()
|
||||||
cmdQueue: make(chan func(), 256),
|
fds := make([]int, 2)
|
||||||
stopChan: make(chan struct{}),
|
if err := unix.Pipe(fds); err != nil {
|
||||||
|
t.Fatalf("failed to create test pipe: %v", err)
|
||||||
}
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
unix.Close(fds[0])
|
||||||
|
unix.Close(fds[1])
|
||||||
|
})
|
||||||
|
return &SharedContext{
|
||||||
|
cmdQueue: make(chan func(), queueSize),
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
wakeR: fds[0],
|
||||||
|
wakeW: fds[1],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSharedContext_ConcurrentPostNonBlocking(t *testing.T) {
|
||||||
|
sc := newTestSharedContext(t, 256)
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
const goroutines = 100
|
const goroutines = 100
|
||||||
@@ -33,10 +49,7 @@ func TestSharedContext_ConcurrentPostNonBlocking(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSharedContext_PostQueueFull(t *testing.T) {
|
func TestSharedContext_PostQueueFull(t *testing.T) {
|
||||||
sc := &SharedContext{
|
sc := newTestSharedContext(t, 2)
|
||||||
cmdQueue: make(chan func(), 2),
|
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
sc.Post(func() {})
|
sc.Post(func() {})
|
||||||
sc.Post(func() {})
|
sc.Post(func() {})
|
||||||
@@ -47,11 +60,8 @@ func TestSharedContext_PostQueueFull(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSharedContext_StartMultipleTimes(t *testing.T) {
|
func TestSharedContext_StartMultipleTimes(t *testing.T) {
|
||||||
sc := &SharedContext{
|
sc := newTestSharedContext(t, 256)
|
||||||
cmdQueue: make(chan func(), 256),
|
sc.started = true
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
started: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
const goroutines = 10
|
const goroutines = 10
|
||||||
@@ -70,10 +80,7 @@ func TestSharedContext_StartMultipleTimes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSharedContext_DrainCmdQueue(t *testing.T) {
|
func TestSharedContext_DrainCmdQueue(t *testing.T) {
|
||||||
sc := &SharedContext{
|
sc := newTestSharedContext(t, 256)
|
||||||
cmdQueue: make(chan func(), 256),
|
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
counter := 0
|
counter := 0
|
||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
@@ -89,10 +96,7 @@ func TestSharedContext_DrainCmdQueue(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSharedContext_DrainCmdQueueEmpty(t *testing.T) {
|
func TestSharedContext_DrainCmdQueueEmpty(t *testing.T) {
|
||||||
sc := &SharedContext{
|
sc := newTestSharedContext(t, 256)
|
||||||
cmdQueue: make(chan func(), 256),
|
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
sc.drainCmdQueue()
|
sc.drainCmdQueue()
|
||||||
|
|
||||||
@@ -100,10 +104,7 @@ func TestSharedContext_DrainCmdQueueEmpty(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSharedContext_ConcurrentDrainAndPost(t *testing.T) {
|
func TestSharedContext_ConcurrentDrainAndPost(t *testing.T) {
|
||||||
sc := &SharedContext{
|
sc := newTestSharedContext(t, 256)
|
||||||
cmdQueue: make(chan func(), 256),
|
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleApplyConfiguration(conn net.Conn, req models.Request, manager *Manager, test bool) {
|
func handleApplyConfiguration(conn net.Conn, req models.Request, manager *Manager, test bool) {
|
||||||
headsParam, ok := req.Params["heads"]
|
headsParam, ok := models.Get[any](req, "heads")
|
||||||
if !ok {
|
if !ok {
|
||||||
models.RespondError(conn, req.ID, "missing 'heads' parameter")
|
models.RespondError(conn, req.ID, "missing 'heads' parameter")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -241,6 +241,7 @@ func (m *Manager) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEven
|
|||||||
handle.SetAdaptiveSyncHandler(func(e wlr_output_management.ZwlrOutputHeadV1AdaptiveSyncEvent) {
|
handle.SetAdaptiveSyncHandler(func(e wlr_output_management.ZwlrOutputHeadV1AdaptiveSyncEvent) {
|
||||||
log.Debugf("WlrOutput: Head %d adaptive sync: %d", headID, e.State)
|
log.Debugf("WlrOutput: Head %d adaptive sync: %d", headID, e.State)
|
||||||
head.adaptiveSync = e.State
|
head.adaptiveSync = e.State
|
||||||
|
head.adaptiveSyncSupported = true
|
||||||
m.post(func() {
|
m.post(func() {
|
||||||
m.updateState()
|
m.updateState()
|
||||||
})
|
})
|
||||||
@@ -360,22 +361,23 @@ func (m *Manager) updateState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
output := Output{
|
output := Output{
|
||||||
Name: head.name,
|
Name: head.name,
|
||||||
Description: head.description,
|
Description: head.description,
|
||||||
Make: head.make,
|
Make: head.make,
|
||||||
Model: head.model,
|
Model: head.model,
|
||||||
SerialNumber: head.serialNumber,
|
SerialNumber: head.serialNumber,
|
||||||
PhysicalWidth: head.physicalWidth,
|
PhysicalWidth: head.physicalWidth,
|
||||||
PhysicalHeight: head.physicalHeight,
|
PhysicalHeight: head.physicalHeight,
|
||||||
Enabled: head.enabled,
|
Enabled: head.enabled,
|
||||||
X: head.x,
|
X: head.x,
|
||||||
Y: head.y,
|
Y: head.y,
|
||||||
Transform: head.transform,
|
Transform: head.transform,
|
||||||
Scale: head.scale,
|
Scale: head.scale,
|
||||||
CurrentMode: currentMode,
|
CurrentMode: currentMode,
|
||||||
Modes: modes,
|
Modes: modes,
|
||||||
AdaptiveSync: head.adaptiveSync,
|
AdaptiveSync: head.adaptiveSync,
|
||||||
ID: head.id,
|
AdaptiveSyncSupported: head.adaptiveSyncSupported,
|
||||||
|
ID: head.id,
|
||||||
}
|
}
|
||||||
outputs = append(outputs, output)
|
outputs = append(outputs, output)
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -17,22 +17,23 @@ type OutputMode struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Output struct {
|
type Output struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Make string `json:"make"`
|
Make string `json:"make"`
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
SerialNumber string `json:"serialNumber"`
|
SerialNumber string `json:"serialNumber"`
|
||||||
PhysicalWidth int32 `json:"physicalWidth"`
|
PhysicalWidth int32 `json:"physicalWidth"`
|
||||||
PhysicalHeight int32 `json:"physicalHeight"`
|
PhysicalHeight int32 `json:"physicalHeight"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
X int32 `json:"x"`
|
X int32 `json:"x"`
|
||||||
Y int32 `json:"y"`
|
Y int32 `json:"y"`
|
||||||
Transform int32 `json:"transform"`
|
Transform int32 `json:"transform"`
|
||||||
Scale float64 `json:"scale"`
|
Scale float64 `json:"scale"`
|
||||||
CurrentMode *OutputMode `json:"currentMode"`
|
CurrentMode *OutputMode `json:"currentMode"`
|
||||||
Modes []OutputMode `json:"modes"`
|
Modes []OutputMode `json:"modes"`
|
||||||
AdaptiveSync uint32 `json:"adaptiveSync"`
|
AdaptiveSync uint32 `json:"adaptiveSync"`
|
||||||
ID uint32 `json:"id"`
|
AdaptiveSyncSupported bool `json:"adaptiveSyncSupported"`
|
||||||
|
ID uint32 `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type State struct {
|
type State struct {
|
||||||
@@ -72,25 +73,26 @@ type Manager struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type headState struct {
|
type headState struct {
|
||||||
id uint32
|
id uint32
|
||||||
handle *wlr_output_management.ZwlrOutputHeadV1
|
handle *wlr_output_management.ZwlrOutputHeadV1
|
||||||
name string
|
name string
|
||||||
description string
|
description string
|
||||||
make string
|
make string
|
||||||
model string
|
model string
|
||||||
serialNumber string
|
serialNumber string
|
||||||
physicalWidth int32
|
physicalWidth int32
|
||||||
physicalHeight int32
|
physicalHeight int32
|
||||||
enabled bool
|
enabled bool
|
||||||
x int32
|
x int32
|
||||||
y int32
|
y int32
|
||||||
transform int32
|
transform int32
|
||||||
scale float64
|
scale float64
|
||||||
currentModeID uint32
|
currentModeID uint32
|
||||||
modeIDs []uint32
|
modeIDs []uint32
|
||||||
adaptiveSync uint32
|
adaptiveSync uint32
|
||||||
finished bool
|
adaptiveSyncSupported bool
|
||||||
ready bool
|
finished bool
|
||||||
|
ready bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type modeState struct {
|
type modeState struct {
|
||||||
@@ -169,7 +171,7 @@ func stateChanged(old, new *State) bool {
|
|||||||
if oldOut.Transform != newOut.Transform || oldOut.Scale != newOut.Scale {
|
if oldOut.Transform != newOut.Transform || oldOut.Scale != newOut.Scale {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if oldOut.AdaptiveSync != newOut.AdaptiveSync {
|
if oldOut.AdaptiveSync != newOut.AdaptiveSync || oldOut.AdaptiveSyncSupported != newOut.AdaptiveSyncSupported {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (oldOut.CurrentMode == nil) != (newOut.CurrentMode == nil) {
|
if (oldOut.CurrentMode == nil) != (newOut.CurrentMode == nil) {
|
||||||
|
|||||||
258
core/internal/themes/manager.go
Normal file
258
core/internal/themes/manager.go
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
package themes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
fs afero.Fs
|
||||||
|
themesDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager() (*Manager, error) {
|
||||||
|
return NewManagerWithFs(afero.NewOsFs())
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManagerWithFs(fs afero.Fs) (*Manager, error) {
|
||||||
|
themesDir := getThemesDir()
|
||||||
|
return &Manager{
|
||||||
|
fs: fs,
|
||||||
|
themesDir: themesDir,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getThemesDir() string {
|
||||||
|
configDir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("failed to get user config dir", "err", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return filepath.Join(configDir, "DankMaterialShell", "themes")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) IsInstalled(theme Theme) (bool, error) {
|
||||||
|
path := m.getInstalledPath(theme.ID)
|
||||||
|
exists, err := afero.Exists(m.fs, path)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return exists, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) getInstalledDir(themeID string) string {
|
||||||
|
return filepath.Join(m.themesDir, themeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) getInstalledPath(themeID string) string {
|
||||||
|
return filepath.Join(m.getInstalledDir(themeID), "theme.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Install(theme Theme, registryThemeDir string) error {
|
||||||
|
themeDir := m.getInstalledDir(theme.ID)
|
||||||
|
|
||||||
|
exists, err := afero.DirExists(m.fs, themeDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check if theme exists: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
return fmt.Errorf("theme already installed: %s", theme.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.fs.MkdirAll(themeDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create theme directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(theme, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal theme: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
themePath := filepath.Join(themeDir, "theme.json")
|
||||||
|
if err := afero.WriteFile(m.fs, themePath, data, 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write theme file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.copyPreviewFiles(registryThemeDir, themeDir, theme)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) copyPreviewFiles(srcDir, dstDir string, theme Theme) {
|
||||||
|
previews := []string{"preview-dark.svg", "preview-light.svg"}
|
||||||
|
|
||||||
|
if theme.Variants != nil {
|
||||||
|
for _, v := range theme.Variants.Options {
|
||||||
|
previews = append(previews,
|
||||||
|
fmt.Sprintf("preview-%s.svg", v.ID),
|
||||||
|
fmt.Sprintf("preview-%s-dark.svg", v.ID),
|
||||||
|
fmt.Sprintf("preview-%s-light.svg", v.ID),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, preview := range previews {
|
||||||
|
srcPath := filepath.Join(srcDir, preview)
|
||||||
|
if exists, _ := afero.Exists(m.fs, srcPath); !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data, err := afero.ReadFile(m.fs, srcPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dstPath := filepath.Join(dstDir, preview)
|
||||||
|
_ = afero.WriteFile(m.fs, dstPath, data, 0644)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) InstallFromRegistry(registry *Registry, themeID string) error {
|
||||||
|
theme, err := registry.Get(themeID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
registryThemeDir := registry.GetThemeDir(theme.SourceDir)
|
||||||
|
return m.Install(*theme, registryThemeDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Update(theme Theme) error {
|
||||||
|
themePath := m.getInstalledPath(theme.ID)
|
||||||
|
|
||||||
|
exists, err := afero.Exists(m.fs, themePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check if theme exists: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("theme not installed: %s", theme.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(theme, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal theme: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := afero.WriteFile(m.fs, themePath, data, 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write theme file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Uninstall(theme Theme) error {
|
||||||
|
return m.UninstallByID(theme.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) UninstallByID(themeID string) error {
|
||||||
|
themeDir := m.getInstalledDir(themeID)
|
||||||
|
|
||||||
|
exists, err := afero.DirExists(m.fs, themeDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check if theme exists: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("theme not installed: %s", themeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.fs.RemoveAll(themeDir); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove theme: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) ListInstalled() ([]string, error) {
|
||||||
|
exists, err := afero.DirExists(m.fs, m.themesDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := afero.ReadDir(m.fs, m.themesDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read themes directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var installed []string
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
themeID := entry.Name()
|
||||||
|
themePath := filepath.Join(m.themesDir, themeID, "theme.json")
|
||||||
|
if exists, _ := afero.Exists(m.fs, themePath); exists {
|
||||||
|
installed = append(installed, themeID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return installed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) GetInstalledTheme(themeID string) (*Theme, error) {
|
||||||
|
themePath := m.getInstalledPath(themeID)
|
||||||
|
|
||||||
|
data, err := afero.ReadFile(m.fs, themePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read theme file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var theme Theme
|
||||||
|
if err := json.Unmarshal(data, &theme); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse theme file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &theme, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) HasUpdates(themeID string, registryTheme Theme) (bool, error) {
|
||||||
|
installed, err := m.GetInstalledTheme(themeID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return compareVersions(installed.Version, registryTheme.Version) < 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareVersions(installed, registry string) int {
|
||||||
|
installedParts := strings.Split(installed, ".")
|
||||||
|
registryParts := strings.Split(registry, ".")
|
||||||
|
|
||||||
|
maxLen := len(installedParts)
|
||||||
|
if len(registryParts) > maxLen {
|
||||||
|
maxLen = len(registryParts)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < maxLen; i++ {
|
||||||
|
var installedNum, registryNum int
|
||||||
|
if i < len(installedParts) {
|
||||||
|
fmt.Sscanf(installedParts[i], "%d", &installedNum)
|
||||||
|
}
|
||||||
|
if i < len(registryParts) {
|
||||||
|
fmt.Sscanf(registryParts[i], "%d", ®istryNum)
|
||||||
|
}
|
||||||
|
|
||||||
|
if installedNum < registryNum {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if installedNum > registryNum {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) GetThemesDir() string {
|
||||||
|
return m.themesDir
|
||||||
|
}
|
||||||
309
core/internal/themes/registry.go
Normal file
309
core/internal/themes/registry.go
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
package themes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v6"
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
)
|
||||||
|
|
||||||
|
const registryRepo = "https://github.com/AvengeMedia/dms-plugin-registry.git"
|
||||||
|
|
||||||
|
type ColorScheme struct {
|
||||||
|
Primary string `json:"primary,omitempty"`
|
||||||
|
PrimaryText string `json:"primaryText,omitempty"`
|
||||||
|
PrimaryContainer string `json:"primaryContainer,omitempty"`
|
||||||
|
Secondary string `json:"secondary,omitempty"`
|
||||||
|
Surface string `json:"surface,omitempty"`
|
||||||
|
SurfaceText string `json:"surfaceText,omitempty"`
|
||||||
|
SurfaceVariant string `json:"surfaceVariant,omitempty"`
|
||||||
|
SurfaceVariantText string `json:"surfaceVariantText,omitempty"`
|
||||||
|
SurfaceTint string `json:"surfaceTint,omitempty"`
|
||||||
|
Background string `json:"background,omitempty"`
|
||||||
|
BackgroundText string `json:"backgroundText,omitempty"`
|
||||||
|
Outline string `json:"outline,omitempty"`
|
||||||
|
SurfaceContainer string `json:"surfaceContainer,omitempty"`
|
||||||
|
SurfaceContainerHigh string `json:"surfaceContainerHigh,omitempty"`
|
||||||
|
SurfaceContainerHighest string `json:"surfaceContainerHighest,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
Warning string `json:"warning,omitempty"`
|
||||||
|
Info string `json:"info,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThemeVariant struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Dark ColorScheme `json:"dark,omitempty"`
|
||||||
|
Light ColorScheme `json:"light,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThemeFlavor struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Dark ColorScheme `json:"dark,omitempty"`
|
||||||
|
Light ColorScheme `json:"light,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThemeAccent struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
FlavorColors map[string]ColorScheme `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ThemeAccent) UnmarshalJSON(data []byte) error {
|
||||||
|
var raw map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(data, &raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.FlavorColors = make(map[string]ColorScheme)
|
||||||
|
var mErr error
|
||||||
|
for key, value := range raw {
|
||||||
|
switch key {
|
||||||
|
case "id":
|
||||||
|
mErr = errors.Join(mErr, json.Unmarshal(value, &a.ID))
|
||||||
|
case "name":
|
||||||
|
mErr = errors.Join(mErr, json.Unmarshal(value, &a.Name))
|
||||||
|
default:
|
||||||
|
var colors ColorScheme
|
||||||
|
if err := json.Unmarshal(value, &colors); err == nil {
|
||||||
|
a.FlavorColors[key] = colors
|
||||||
|
} else {
|
||||||
|
mErr = errors.Join(mErr, fmt.Errorf("failed to unmarshal flavor colors for key %s: %w", key, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a ThemeAccent) MarshalJSON() ([]byte, error) {
|
||||||
|
m := map[string]any{
|
||||||
|
"id": a.ID,
|
||||||
|
"name": a.Name,
|
||||||
|
}
|
||||||
|
for k, v := range a.FlavorColors {
|
||||||
|
m[k] = v
|
||||||
|
}
|
||||||
|
return json.Marshal(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MultiVariantDefaults struct {
|
||||||
|
Dark map[string]string `json:"dark,omitempty"`
|
||||||
|
Light map[string]string `json:"light,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThemeVariants struct {
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Default string `json:"default,omitempty"`
|
||||||
|
Defaults *MultiVariantDefaults `json:"defaults,omitempty"`
|
||||||
|
Options []ThemeVariant `json:"options,omitempty"`
|
||||||
|
Flavors []ThemeFlavor `json:"flavors,omitempty"`
|
||||||
|
Accents []ThemeAccent `json:"accents,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Theme struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Dark ColorScheme `json:"dark"`
|
||||||
|
Light ColorScheme `json:"light"`
|
||||||
|
Variants *ThemeVariants `json:"variants,omitempty"`
|
||||||
|
PreviewPath string `json:"-"`
|
||||||
|
SourceDir string `json:"sourceDir,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GitClient interface {
|
||||||
|
PlainClone(path string, url string) error
|
||||||
|
Pull(path string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type realGitClient struct{}
|
||||||
|
|
||||||
|
func (g *realGitClient) PlainClone(path string, url string) error {
|
||||||
|
_, err := git.PlainClone(path, &git.CloneOptions{
|
||||||
|
URL: url,
|
||||||
|
Progress: os.Stdout,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *realGitClient) Pull(path string) error {
|
||||||
|
repo, err := git.PlainOpen(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
worktree, err := repo.Worktree()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = worktree.Pull(&git.PullOptions{})
|
||||||
|
if err != nil && err.Error() != "already up-to-date" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Registry struct {
|
||||||
|
fs afero.Fs
|
||||||
|
cacheDir string
|
||||||
|
themes []Theme
|
||||||
|
git GitClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegistry() (*Registry, error) {
|
||||||
|
return NewRegistryWithFs(afero.NewOsFs())
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegistryWithFs(fs afero.Fs) (*Registry, error) {
|
||||||
|
cacheDir := getCacheDir()
|
||||||
|
return &Registry{
|
||||||
|
fs: fs,
|
||||||
|
cacheDir: cacheDir,
|
||||||
|
git: &realGitClient{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCacheDir() string {
|
||||||
|
return filepath.Join(os.TempDir(), "dankdots-plugin-registry")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) Update() error {
|
||||||
|
exists, err := afero.DirExists(r.fs, r.cacheDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check cache directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.git.PlainClone(r.cacheDir, registryRepo); err != nil {
|
||||||
|
return fmt.Errorf("failed to clone registry: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := r.git.Pull(r.cacheDir); err != nil {
|
||||||
|
if err := r.fs.RemoveAll(r.cacheDir); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove corrupted registry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.git.PlainClone(r.cacheDir, registryRepo); err != nil {
|
||||||
|
return fmt.Errorf("failed to re-clone registry: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.loadThemes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) loadThemes() error {
|
||||||
|
themesDir := filepath.Join(r.cacheDir, "themes")
|
||||||
|
|
||||||
|
entries, err := afero.ReadDir(r.fs, themesDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read themes directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.themes = []Theme{}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
themeDir := filepath.Join(themesDir, entry.Name())
|
||||||
|
themeFile := filepath.Join(themeDir, "theme.json")
|
||||||
|
|
||||||
|
data, err := afero.ReadFile(r.fs, themeFile)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var theme Theme
|
||||||
|
if err := json.Unmarshal(data, &theme); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if theme.ID == "" {
|
||||||
|
theme.ID = entry.Name()
|
||||||
|
}
|
||||||
|
theme.SourceDir = entry.Name()
|
||||||
|
|
||||||
|
previewPath := filepath.Join(themeDir, "preview.svg")
|
||||||
|
if exists, _ := afero.Exists(r.fs, previewPath); exists {
|
||||||
|
theme.PreviewPath = previewPath
|
||||||
|
}
|
||||||
|
|
||||||
|
r.themes = append(r.themes, theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) List() ([]Theme, error) {
|
||||||
|
if len(r.themes) == 0 {
|
||||||
|
if err := r.Update(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SortByFirstParty(r.themes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) Search(query string) ([]Theme, error) {
|
||||||
|
allThemes, err := r.List()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if query == "" {
|
||||||
|
return allThemes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return SortByFirstParty(FuzzySearch(query, allThemes)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) Get(idOrName string) (*Theme, error) {
|
||||||
|
themes, err := r.List()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range themes {
|
||||||
|
if t.ID == idOrName {
|
||||||
|
return &t, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range themes {
|
||||||
|
if t.Name == idOrName {
|
||||||
|
return &t, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("theme not found: %s", idOrName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) GetThemeSourcePath(themeID string) string {
|
||||||
|
return filepath.Join(r.cacheDir, "themes", themeID, "theme.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) GetThemeDir(themeID string) string {
|
||||||
|
return filepath.Join(r.cacheDir, "themes", themeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SortByFirstParty(themes []Theme) []Theme {
|
||||||
|
return themes
|
||||||
|
}
|
||||||
40
core/internal/themes/search.go
Normal file
40
core/internal/themes/search.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package themes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FuzzySearch(query string, themes []Theme) []Theme {
|
||||||
|
if query == "" {
|
||||||
|
return themes
|
||||||
|
}
|
||||||
|
|
||||||
|
queryLower := strings.ToLower(query)
|
||||||
|
return utils.Filter(themes, func(t Theme) bool {
|
||||||
|
return fuzzyMatch(queryLower, strings.ToLower(t.Name)) ||
|
||||||
|
fuzzyMatch(queryLower, strings.ToLower(t.Description)) ||
|
||||||
|
fuzzyMatch(queryLower, strings.ToLower(t.Author))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func fuzzyMatch(query, text string) bool {
|
||||||
|
queryIdx := 0
|
||||||
|
for _, char := range text {
|
||||||
|
if queryIdx < len(query) && char == rune(query[queryIdx]) {
|
||||||
|
queryIdx++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return queryIdx == len(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindByIDOrName(idOrName string, themes []Theme) *Theme {
|
||||||
|
if t, found := utils.Find(themes, func(t Theme) bool { return t.ID == idOrName }); found {
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
if t, found := utils.Find(themes, func(t Theme) bool { return t.Name == idOrName }); found {
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,8 +1,58 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import "os/exec"
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AppChecker interface {
|
||||||
|
CommandExists(cmd string) bool
|
||||||
|
AnyCommandExists(cmds ...string) bool
|
||||||
|
FlatpakExists(name string) bool
|
||||||
|
AnyFlatpakExists(flatpaks ...string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type DefaultAppChecker struct{}
|
||||||
|
|
||||||
|
func (DefaultAppChecker) CommandExists(cmd string) bool {
|
||||||
|
return CommandExists(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (DefaultAppChecker) AnyCommandExists(cmds ...string) bool {
|
||||||
|
return AnyCommandExists(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (DefaultAppChecker) FlatpakExists(name string) bool {
|
||||||
|
return FlatpakExists(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (DefaultAppChecker) AnyFlatpakExists(flatpaks ...string) bool {
|
||||||
|
return AnyFlatpakExists(flatpaks...)
|
||||||
|
}
|
||||||
|
|
||||||
func CommandExists(cmd string) bool {
|
func CommandExists(cmd string) bool {
|
||||||
_, err := exec.LookPath(cmd)
|
_, err := exec.LookPath(cmd)
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AnyCommandExists(cmds ...string) bool {
|
||||||
|
for _, cmd := range cmds {
|
||||||
|
if CommandExists(cmd) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsServiceActive(name string, userService bool) bool {
|
||||||
|
if !CommandExists("systemctl") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"is-active", name}
|
||||||
|
if userService {
|
||||||
|
args = []string{"--user", "is-active", name}
|
||||||
|
}
|
||||||
|
output, _ := exec.Command("systemctl", args...).Output()
|
||||||
|
return strings.EqualFold(strings.TrimSpace(string(output)), "active")
|
||||||
|
}
|
||||||
|
|||||||
83
core/internal/utils/flatpak.go
Normal file
83
core/internal/utils/flatpak.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"os/exec"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FlatpakInPath() bool {
|
||||||
|
_, err := exec.LookPath("flatpak")
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FlatpakExists(name string) bool {
|
||||||
|
if !FlatpakInPath() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("flatpak", "info", name)
|
||||||
|
err := cmd.Run()
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FlatpakSearchBySubstring(substring string) bool {
|
||||||
|
if !FlatpakInPath() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("flatpak", "list", "--app")
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
out := stdout.String()
|
||||||
|
|
||||||
|
for line := range strings.SplitSeq(out, "\n") {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) > 1 {
|
||||||
|
id := fields[1]
|
||||||
|
idParts := strings.Split(id, ".")
|
||||||
|
// We are assuming that the last part of the ID is
|
||||||
|
// the package name we're looking for. This might
|
||||||
|
// not always be true, some developers use arbitrary
|
||||||
|
// suffixes.
|
||||||
|
if len(idParts) > 0 && idParts[len(idParts)-1] == substring {
|
||||||
|
cmd := exec.Command("flatpak", "info", id)
|
||||||
|
err := cmd.Run()
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func AnyFlatpakExists(flatpaks ...string) bool {
|
||||||
|
return slices.ContainsFunc(flatpaks, FlatpakExists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FlatpakInstallationDir(name string) (string, error) {
|
||||||
|
if !FlatpakInPath() {
|
||||||
|
return "", errors.New("flatpak not found in PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("flatpak", "info", "--show-location", name)
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return "", errors.New("flatpak not installed: " + name)
|
||||||
|
}
|
||||||
|
|
||||||
|
location := strings.TrimSpace(stdout.String())
|
||||||
|
if location == "" {
|
||||||
|
return "", errors.New("installation directory not found for: " + name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return location, nil
|
||||||
|
}
|
||||||
271
core/internal/utils/flatpak_test.go
Normal file
271
core/internal/utils/flatpak_test.go
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFlatpakInPathAvailable(t *testing.T) {
|
||||||
|
result := FlatpakInPath()
|
||||||
|
if !result {
|
||||||
|
t.Skip("flatpak not in PATH")
|
||||||
|
}
|
||||||
|
if !result {
|
||||||
|
t.Errorf("expected true when flatpak is in PATH")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFlatpakInPathUnavailable(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
t.Setenv("PATH", tempDir)
|
||||||
|
|
||||||
|
result := FlatpakInPath()
|
||||||
|
if result {
|
||||||
|
t.Errorf("expected false when flatpak not in PATH, got true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFlatpakExistsValidPackage(t *testing.T) {
|
||||||
|
if !FlatpakInPath() {
|
||||||
|
t.Skip("flatpak not in PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
result := FlatpakExists("com.nonexistent.package.test")
|
||||||
|
if result {
|
||||||
|
t.Logf("package exists (unexpected but not an error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFlatpakExistsNoFlatpak(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
t.Setenv("PATH", tempDir)
|
||||||
|
|
||||||
|
result := FlatpakExists("any.package.name")
|
||||||
|
if result {
|
||||||
|
t.Errorf("expected false when flatpak not in PATH, got true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFlatpakSearchBySubstringNoFlatpak(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
t.Setenv("PATH", tempDir)
|
||||||
|
|
||||||
|
result := FlatpakSearchBySubstring("test")
|
||||||
|
if result {
|
||||||
|
t.Errorf("expected false when flatpak not in PATH, got true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFlatpakSearchBySubstringNonexistent(t *testing.T) {
|
||||||
|
if !FlatpakInPath() {
|
||||||
|
t.Skip("flatpak not in PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
result := FlatpakSearchBySubstring("ThisIsAVeryUnlikelyPackageName12345")
|
||||||
|
if result {
|
||||||
|
t.Errorf("expected false for nonexistent package substring")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFlatpakInstallationDirNoFlatpak(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
t.Setenv("PATH", tempDir)
|
||||||
|
|
||||||
|
_, err := FlatpakInstallationDir("any.package.name")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error when flatpak not in PATH")
|
||||||
|
}
|
||||||
|
if err != nil && !strings.Contains(err.Error(), "not found in PATH") {
|
||||||
|
t.Errorf("expected 'not found in PATH' error, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFlatpakInstallationDirNonexistent(t *testing.T) {
|
||||||
|
if !FlatpakInPath() {
|
||||||
|
t.Skip("flatpak not in PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := FlatpakInstallationDir("com.nonexistent.package.test")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error for nonexistent package")
|
||||||
|
}
|
||||||
|
if err != nil && !strings.Contains(err.Error(), "not installed") {
|
||||||
|
t.Errorf("expected 'not installed' error, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFlatpakInstallationDirValid(t *testing.T) {
|
||||||
|
if !FlatpakInPath() {
|
||||||
|
t.Skip("flatpak not in PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test requires a known installed flatpak
|
||||||
|
// We can't guarantee any specific flatpak is installed,
|
||||||
|
// so we'll skip if we can't find a common one
|
||||||
|
commonFlatpaks := []string{
|
||||||
|
"org.mozilla.firefox",
|
||||||
|
"org.gnome.Calculator",
|
||||||
|
"org.freedesktop.Platform",
|
||||||
|
}
|
||||||
|
|
||||||
|
var testPackage string
|
||||||
|
for _, pkg := range commonFlatpaks {
|
||||||
|
if FlatpakExists(pkg) {
|
||||||
|
testPackage = pkg
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if testPackage == "" {
|
||||||
|
t.Skip("no common flatpak packages found for testing")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := FlatpakInstallationDir(testPackage)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if result == "" {
|
||||||
|
t.Errorf("expected non-empty installation directory")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result, testPackage) {
|
||||||
|
t.Logf("installation directory %s doesn't contain package name (may be expected)", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFlatpakExistsCommandFailure(t *testing.T) {
|
||||||
|
if !FlatpakInPath() {
|
||||||
|
t.Skip("flatpak not in PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock a failing flatpak command through PATH interception
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
fakeFlatpak := filepath.Join(tempDir, "flatpak")
|
||||||
|
|
||||||
|
script := "#!/bin/sh\nexit 1\n"
|
||||||
|
err := os.WriteFile(fakeFlatpak, []byte(script), 0755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create fake flatpak: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
originalPath := os.Getenv("PATH")
|
||||||
|
t.Setenv("PATH", tempDir+":"+originalPath)
|
||||||
|
|
||||||
|
result := FlatpakExists("test.package")
|
||||||
|
if result {
|
||||||
|
t.Errorf("expected false when flatpak command fails, got true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFlatpakSearchBySubstringCommandFailure(t *testing.T) {
|
||||||
|
if !FlatpakInPath() {
|
||||||
|
t.Skip("flatpak not in PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock a failing flatpak command through PATH interception
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
fakeFlatpak := filepath.Join(tempDir, "flatpak")
|
||||||
|
|
||||||
|
script := "#!/bin/sh\nexit 1\n"
|
||||||
|
err := os.WriteFile(fakeFlatpak, []byte(script), 0755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create fake flatpak: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
originalPath := os.Getenv("PATH")
|
||||||
|
t.Setenv("PATH", tempDir+":"+originalPath)
|
||||||
|
|
||||||
|
result := FlatpakSearchBySubstring("test")
|
||||||
|
if result {
|
||||||
|
t.Errorf("expected false when flatpak command fails, got true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFlatpakInstallationDirCommandFailure(t *testing.T) {
|
||||||
|
if !FlatpakInPath() {
|
||||||
|
t.Skip("flatpak not in PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock a failing flatpak command through PATH interception
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
fakeFlatpak := filepath.Join(tempDir, "flatpak")
|
||||||
|
|
||||||
|
script := "#!/bin/sh\nexit 1\n"
|
||||||
|
err := os.WriteFile(fakeFlatpak, []byte(script), 0755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create fake flatpak: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
originalPath := os.Getenv("PATH")
|
||||||
|
t.Setenv("PATH", tempDir+":"+originalPath)
|
||||||
|
|
||||||
|
_, err = FlatpakInstallationDir("test.package")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error when flatpak command fails")
|
||||||
|
}
|
||||||
|
if err != nil && !strings.Contains(err.Error(), "not installed") {
|
||||||
|
t.Errorf("expected 'not installed' error, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnyFlatpakExistsSomeExist(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
fakeFlatpak := filepath.Join(tempDir, "flatpak")
|
||||||
|
|
||||||
|
// Script that succeeds only for "app.exists.test"
|
||||||
|
script := `#!/bin/sh
|
||||||
|
if [ "$1" = "info" ] && [ "$2" = "app.exists.test" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
`
|
||||||
|
err := os.WriteFile(fakeFlatpak, []byte(script), 0755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create fake flatpak: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
originalPath := os.Getenv("PATH")
|
||||||
|
t.Setenv("PATH", tempDir+":"+originalPath)
|
||||||
|
|
||||||
|
result := AnyFlatpakExists("com.nonexistent.flatpak", "app.exists.test", "com.another.nonexistent")
|
||||||
|
if !result {
|
||||||
|
t.Errorf("expected true when at least one flatpak exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnyFlatpakExistsNoneExist(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
fakeFlatpak := filepath.Join(tempDir, "flatpak")
|
||||||
|
|
||||||
|
script := "#!/bin/sh\nexit 1\n"
|
||||||
|
err := os.WriteFile(fakeFlatpak, []byte(script), 0755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create fake flatpak: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
originalPath := os.Getenv("PATH")
|
||||||
|
t.Setenv("PATH", tempDir+":"+originalPath)
|
||||||
|
|
||||||
|
result := AnyFlatpakExists("com.nonexistent.flatpak1", "com.nonexistent.flatpak2")
|
||||||
|
if result {
|
||||||
|
t.Errorf("expected false when no flatpaks exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnyFlatpakExistsNoFlatpak(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
t.Setenv("PATH", tempDir)
|
||||||
|
|
||||||
|
result := AnyFlatpakExists("any.package.name", "another.package")
|
||||||
|
if result {
|
||||||
|
t.Errorf("expected false when flatpak not in PATH, got true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnyFlatpakExistsEmpty(t *testing.T) {
|
||||||
|
result := AnyFlatpakExists()
|
||||||
|
if result {
|
||||||
|
t.Errorf("expected false when no flatpaks specified")
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user