mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-03 02:52:07 -04:00
Compare commits
120 Commits
ac0a8f3449
...
v1.4.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f24cf37ca | ||
|
|
01218f34cb | ||
|
|
9da58d8296 | ||
|
|
af0038e634 | ||
|
|
05c312b9eb | ||
|
|
89d5c958c4 | ||
|
|
e4d86ad595 | ||
|
|
532b54a028 | ||
|
|
504d027c3f | ||
|
|
e8f95f4533 | ||
|
|
b83256c83a | ||
|
|
8e2cd21be8 | ||
|
|
c5413608da | ||
|
|
586bcad442 | ||
|
|
3b3d10f730 | ||
|
|
4834891b36 | ||
|
|
f60e65aecb | ||
|
|
01387b0123 | ||
|
|
1476658c23 | ||
|
|
7861c6e316 | ||
|
|
d2247d7b24 | ||
|
|
2ff78d4a02 | ||
|
|
785243ce5f | ||
|
|
0e1b868384 | ||
|
|
2b08e800e8 | ||
|
|
74e4f8ea1e | ||
|
|
9c58569b4c | ||
|
|
29de677e00 | ||
|
|
fae4944845 | ||
|
|
07a0ac4b7d | ||
|
|
b2d8f4d73b | ||
|
|
fe58c45233 | ||
|
|
3ea4e389eb | ||
|
|
7276f295fc | ||
|
|
93ed96a789 | ||
|
|
bea325e94c | ||
|
|
2f8f1c30ad | ||
|
|
f859a14173 | ||
|
|
153f39da48 | ||
|
|
e4accdd1c7 | ||
|
|
a2c89e0a8c | ||
|
|
e282831c2e | ||
|
|
5c5ff6195a | ||
|
|
c4bbf54679 | ||
|
|
98acafb4b8 | ||
|
|
da20681fc0 | ||
|
|
b38cb961b2 | ||
|
|
7a0bb07518 | ||
|
|
403e3e90a2 | ||
|
|
50b91f14b6 | ||
|
|
b3df47fce0 | ||
|
|
09bd65d746 | ||
|
|
020d56ab7f | ||
|
|
f3bee65da9 | ||
|
|
b14b0946e2 | ||
|
|
ca44205f1c | ||
|
|
2d39e8fd2a | ||
|
|
6d4df6e927 | ||
|
|
b8ab86e6c0 | ||
|
|
837329a6d8 | ||
|
|
8c6c2ffd23 | ||
|
|
ad3c8b6755 | ||
|
|
03a8e1e0d5 | ||
|
|
4d4d3c20a1 | ||
|
|
cef16d6bc9 | ||
|
|
aafaad1791 | ||
|
|
7906fdc2b0 | ||
|
|
397650ca52 | ||
|
|
826207006a | ||
|
|
58c2fcd31c | ||
|
|
b2a2b425ec | ||
|
|
942c9c9609 | ||
|
|
46d6e1cff3 | ||
|
|
a4137c57c1 | ||
|
|
1ad8b627f1 | ||
|
|
58a02ce290 | ||
|
|
8e1ad1a2be | ||
|
|
68cd7ab32c | ||
|
|
f649ce9a8e | ||
|
|
c4df242f07 | ||
|
|
26846c8d55 | ||
|
|
31b44a667c | ||
|
|
4f3b73ee21 | ||
|
|
4cfae91f02 | ||
|
|
8d947a6e95 | ||
|
|
1e84d4252c | ||
|
|
76072e1d4c | ||
|
|
6408dce4a9 | ||
|
|
0b2e1cca38 | ||
|
|
c1bfd8c0b7 | ||
|
|
90ffa5833b | ||
|
|
169c669286 | ||
|
|
f8350deafc | ||
|
|
0286a1b80b | ||
|
|
7c3e6c1f02 | ||
|
|
d2d72db3c9 | ||
|
|
f81f861408 | ||
|
|
af494543f5 | ||
|
|
db4de55338 | ||
|
|
37ecbbbbde | ||
|
|
d6a6d2a438 | ||
|
|
bf1c6eec74 | ||
|
|
0ddae80584 | ||
|
|
5c96c03bfa | ||
|
|
dfe36e47d8 | ||
|
|
63e1b75e57 | ||
|
|
29efdd8598 | ||
|
|
34d03cf11b | ||
|
|
c339389d44 | ||
|
|
af5f6eb656 | ||
|
|
a6d28e2553 | ||
|
|
6213267908 | ||
|
|
d084114149 | ||
|
|
f6d99eca0d | ||
|
|
722eb3289e | ||
|
|
b7f2bdcb2d | ||
|
|
11c20db6e6 | ||
|
|
8a4e3f8bb1 | ||
|
|
bc8fe97c13 | ||
|
|
47262155aa |
73
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
73
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -7,32 +7,32 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
## DankMaterialShell Bug Report
|
||||
Limit your report to one issue per submission unless closely related
|
||||
- type: checkboxes
|
||||
Limit your report to one issue per submission unless similarly related
|
||||
- type: dropdown
|
||||
id: compositor
|
||||
attributes:
|
||||
label: Compositor
|
||||
options:
|
||||
- label: Niri
|
||||
- label: Hyprland
|
||||
- label: MangoWC (dwl)
|
||||
- label: Sway
|
||||
- Niri
|
||||
- Hyprland
|
||||
- MangoWC (dwl)
|
||||
- Sway
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
- type: dropdown
|
||||
id: distribution
|
||||
attributes:
|
||||
label: Distribution
|
||||
options:
|
||||
- label: Arch Linux
|
||||
- label: CachyOS
|
||||
- label: Fedora
|
||||
- label: NixOS
|
||||
- label: Debian
|
||||
- label: Ubuntu
|
||||
- label: Gentoo
|
||||
- label: OpenSUSE
|
||||
- label: Other (specify below)
|
||||
- Arch Linux
|
||||
- CachyOS
|
||||
- Fedora
|
||||
- NixOS
|
||||
- Debian
|
||||
- Ubuntu
|
||||
- Gentoo
|
||||
- OpenSUSE
|
||||
- Other (specify below)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
@@ -42,12 +42,45 @@ body:
|
||||
placeholder: e.g., PikaOS, Void Linux, etc.
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: installation_method
|
||||
attributes:
|
||||
label: Select your Installation Method
|
||||
options:
|
||||
- DankInstaller
|
||||
- Distro Packaging
|
||||
- Source
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: original_installation_method
|
||||
attributes:
|
||||
label: Was this your original Installation method?
|
||||
options:
|
||||
- "Yes"
|
||||
- No (specify below)
|
||||
default: 0
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: original_installation_method_specify
|
||||
attributes:
|
||||
label: If no, specify
|
||||
placeholder: e.g., Distro Packaging, then Source
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: dms_doctor
|
||||
attributes:
|
||||
label: dms doctor -v
|
||||
description: Output of `dms doctor -v` command
|
||||
placeholder: Paste the output of `dms doctor -v` here
|
||||
label: dms doctor -vC
|
||||
description: Output of `dms doctor -vC` command — paste between the details tags below to keep it collapsed in the issue
|
||||
placeholder: Paste the output of `dms doctor -vC` here
|
||||
value: |
|
||||
<details>
|
||||
<summary>Click to expand</summary>
|
||||
|
||||
|
||||
</details>
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -69,7 +102,7 @@ body:
|
||||
- type: textarea
|
||||
id: steps_to_reproduce
|
||||
attributes:
|
||||
label: Steps to Reproduce & Installation Method
|
||||
label: Steps to Reproduce
|
||||
description: Please provide detailed steps to reproduce the issue
|
||||
placeholder: |
|
||||
1. ...
|
||||
|
||||
21
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
21
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -23,18 +23,25 @@ body:
|
||||
placeholder: Why is this feature important?
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
- type: dropdown
|
||||
id: compositor
|
||||
attributes:
|
||||
label: Compositor(s)
|
||||
description: Is this feature specific to one or more compositors?
|
||||
options:
|
||||
- label: All compositors
|
||||
- label: Niri
|
||||
- label: Hyprland
|
||||
- label: MangoWC (dwl)
|
||||
- label: Sway
|
||||
- label: Other (specify below)
|
||||
- All compositors
|
||||
- Niri
|
||||
- Hyprland
|
||||
- MangoWC (dwl)
|
||||
- Sway
|
||||
- Other (specify below)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: compositor_other
|
||||
attributes:
|
||||
label: If Other, please specify
|
||||
placeholder: e.g., Wayfire, Mutter, etc.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
|
||||
79
.github/ISSUE_TEMPLATE/support_request.yml
vendored
79
.github/ISSUE_TEMPLATE/support_request.yml
vendored
@@ -7,32 +7,87 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
## DankMaterialShell Support Request
|
||||
- type: checkboxes
|
||||
- type: dropdown
|
||||
id: compositor
|
||||
attributes:
|
||||
label: Compositor
|
||||
options:
|
||||
- label: Niri
|
||||
- label: Hyprland
|
||||
- label: MangoWC (dwl)
|
||||
- label: Sway
|
||||
- label: Other (specify below)
|
||||
- Niri
|
||||
- Hyprland
|
||||
- MangoWC (dwl)
|
||||
- Sway
|
||||
- Other (specify below)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: compositor_other
|
||||
attributes:
|
||||
label: If Other, please specify
|
||||
placeholder: e.g., Wayfire, Mutter, etc.
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
- type: dropdown
|
||||
id: distribution
|
||||
attributes:
|
||||
label: Distribution
|
||||
description: Which Linux distribution are you using? (e.g., Arch, Fedora, Debian, etc.)
|
||||
placeholder: Your Linux distribution
|
||||
options:
|
||||
- Arch Linux
|
||||
- CachyOS
|
||||
- Fedora
|
||||
- NixOS
|
||||
- Debian
|
||||
- Ubuntu
|
||||
- Gentoo
|
||||
- OpenSUSE
|
||||
- Other (specify below)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: distribution_other
|
||||
attributes:
|
||||
label: If Other, please specify
|
||||
placeholder: e.g., PikaOS, Void Linux, etc.
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: installation_method
|
||||
attributes:
|
||||
label: Select your Installation Method
|
||||
options:
|
||||
- DankInstaller
|
||||
- Distro Packaging
|
||||
- Source
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: original_installation_method_different
|
||||
attributes:
|
||||
label: Was your original Installation method different?
|
||||
options:
|
||||
- "Yes"
|
||||
- No (specify below)
|
||||
default: 0
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: original_installation_method_specify
|
||||
attributes:
|
||||
label: If no, specify
|
||||
placeholder: e.g., Distro Packaging, then Source
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: dms_doctor
|
||||
attributes:
|
||||
label: dms doctor -v
|
||||
description: Output of `dms doctor -v` command
|
||||
placeholder: Paste the output of `dms doctor -v` here
|
||||
label: dms doctor -vC
|
||||
description: Output of `dms doctor -vC` command — paste between the lines below to keep it collapsed in the issue
|
||||
placeholder: Paste the output of `dms doctor -vC` here
|
||||
value: |
|
||||
<details>
|
||||
<summary>Click to expand</summary>
|
||||
|
||||
|
||||
</details>
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
|
||||
383
.github/workflows/backup/run-obs.yml.bak
vendored
383
.github/workflows/backup/run-obs.yml.bak
vendored
@@ -1,383 +0,0 @@
|
||||
name: Update OBS Packages
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: "Package to update (dms, dms-git, or all)"
|
||||
required: false
|
||||
default: "all"
|
||||
force_upload:
|
||||
description: "Force upload without version check"
|
||||
required: false
|
||||
default: "false"
|
||||
type: choice
|
||||
options:
|
||||
- "false"
|
||||
- "true"
|
||||
rebuild_release:
|
||||
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
|
||||
required: false
|
||||
default: ""
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
schedule:
|
||||
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
|
||||
|
||||
jobs:
|
||||
check-updates:
|
||||
name: Check for updates
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
has_updates: ${{ steps.check.outputs.has_updates }}
|
||||
packages: ${{ steps.check.outputs.packages }}
|
||||
version: ${{ steps.check.outputs.version }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install OSC
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y osc
|
||||
|
||||
mkdir -p ~/.config/osc
|
||||
cat > ~/.config/osc/oscrc << EOF
|
||||
[general]
|
||||
apiurl = https://api.opensuse.org
|
||||
|
||||
[https://api.opensuse.org]
|
||||
user = ${{ secrets.OBS_USERNAME }}
|
||||
pass = ${{ secrets.OBS_PASSWORD }}
|
||||
EOF
|
||||
chmod 600 ~/.config/osc/oscrc
|
||||
|
||||
- name: Check for updates
|
||||
id: check
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "Triggered by tag: $VERSION (always update)"
|
||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||
echo "Checking if dms-git source has changed..."
|
||||
|
||||
# Get current commit hash (8 chars to match spec format)
|
||||
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||
|
||||
# Check OBS for last uploaded commit
|
||||
OBS_BASE="$HOME/.cache/osc-checkouts"
|
||||
mkdir -p "$OBS_BASE"
|
||||
OBS_PROJECT="home:AvengeMedia:dms-git"
|
||||
|
||||
if [[ -d "$OBS_BASE/$OBS_PROJECT/dms-git" ]]; then
|
||||
cd "$OBS_BASE/$OBS_PROJECT/dms-git"
|
||||
osc up -q 2>/dev/null || true
|
||||
|
||||
# Extract commit hash from spec Version line & format like; 0.6.2+git2264.a679be68
|
||||
if [[ -f "dms-git.spec" ]]; then
|
||||
OBS_COMMIT=$(grep "^Version:" "dms-git.spec" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
||||
|
||||
if [[ -n "$OBS_COMMIT" ]]; then
|
||||
if [[ "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
echo "📋 Commit $CURRENT_COMMIT already uploaded to OBS, skipping"
|
||||
else
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "📋 New commit detected: $CURRENT_COMMIT (OBS has $OBS_COMMIT)"
|
||||
fi
|
||||
else
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "📋 Could not extract OBS commit, proceeding with update"
|
||||
fi
|
||||
else
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "📋 No spec file in OBS, proceeding with update"
|
||||
fi
|
||||
|
||||
cd "${{ github.workspace }}"
|
||||
else
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "📋 First upload to OBS, update needed"
|
||||
fi
|
||||
elif [[ "${{ github.event.inputs.force_upload }}" == "true" ]]; then
|
||||
PKG="${{ github.event.inputs.package }}"
|
||||
if [[ -z "$PKG" || "$PKG" == "all" ]]; then
|
||||
echo "packages=all" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "🚀 Force upload: all packages"
|
||||
else
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "🚀 Force upload: $PKG"
|
||||
fi
|
||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||
else
|
||||
echo "packages=all" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
update-obs:
|
||||
name: Upload to OBS
|
||||
needs: check-updates
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
if: |
|
||||
github.event.inputs.force_upload == 'true' ||
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
needs.check-updates.outputs.has_updates == 'true'
|
||||
|
||||
steps:
|
||||
- name: Generate GitHub App Token
|
||||
id: generate_token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
|
||||
- name: Check if last commit was automated
|
||||
id: check-loop
|
||||
run: |
|
||||
LAST_COMMIT_MSG=$(git log -1 --pretty=%B | head -1)
|
||||
if [[ "$LAST_COMMIT_MSG" == "ci: Auto-update PPA packages"* ]] || [[ "$LAST_COMMIT_MSG" == "ci: Auto-update OBS packages"* ]]; then
|
||||
echo "⏭️ Last commit was automated ($LAST_COMMIT_MSG), skipping to prevent infinite loop"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "✅ Last commit was not automated, proceeding"
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Determine packages to update
|
||||
if: steps.check-loop.outputs.skip != 'true'
|
||||
id: packages
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Triggered by tag: $VERSION"
|
||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
echo "Triggered by schedule: updating git package"
|
||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||
else
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Update dms-git spec version
|
||||
if: steps.check-loop.outputs.skip != 'true' && (contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all')
|
||||
run: |
|
||||
# Get commit info for dms-git versioning
|
||||
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
|
||||
|
||||
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
||||
echo "📦 Updating dms-git.spec to version: $NEW_VERSION"
|
||||
|
||||
# Update version in spec
|
||||
sed -i "s/^Version:.*/Version: $NEW_VERSION/" distro/opensuse/dms-git.spec
|
||||
|
||||
# Add changelog entry
|
||||
DATE_STR=$(date "+%a %b %d %Y")
|
||||
CHANGELOG_ENTRY="* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1\n- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
|
||||
sed -i "/%changelog/a\\$CHANGELOG_ENTRY" distro/opensuse/dms-git.spec
|
||||
|
||||
- name: Update Debian dms-git changelog version
|
||||
if: steps.check-loop.outputs.skip != 'true' && (contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all')
|
||||
run: |
|
||||
# Get commit info for dms-git versioning
|
||||
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
|
||||
|
||||
# Debian version format: 0.6.2+git2256.9162e314
|
||||
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
||||
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
|
||||
|
||||
CHANGELOG_DATE=$(date -R)
|
||||
|
||||
CHANGELOG_FILE="distro/debian/dms-git/debian/changelog"
|
||||
|
||||
# Get current version from changelog
|
||||
CURRENT_VERSION=$(head -1 "$CHANGELOG_FILE" | sed 's/.*(\([^)]*\)).*/\1/')
|
||||
|
||||
echo "Current Debian version: $CURRENT_VERSION"
|
||||
echo "New version: $NEW_VERSION"
|
||||
|
||||
# Only update if version changed
|
||||
if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
|
||||
# Create new changelog entry at top
|
||||
TEMP_CHANGELOG=$(mktemp)
|
||||
|
||||
cat > "$TEMP_CHANGELOG" << EOF
|
||||
dms-git ($NEW_VERSION) nightly; urgency=medium
|
||||
|
||||
* Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)
|
||||
|
||||
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
|
||||
|
||||
EOF
|
||||
|
||||
# Prepend to existing changelog
|
||||
cat "$CHANGELOG_FILE" >> "$TEMP_CHANGELOG"
|
||||
mv "$TEMP_CHANGELOG" "$CHANGELOG_FILE"
|
||||
|
||||
echo "✓ Updated Debian changelog: $CURRENT_VERSION → $NEW_VERSION"
|
||||
else
|
||||
echo "✓ Debian changelog already at version $NEW_VERSION"
|
||||
fi
|
||||
|
||||
- name: Update dms stable version
|
||||
if: steps.check-loop.outputs.skip != 'true' && steps.packages.outputs.version != ''
|
||||
run: |
|
||||
VERSION="${{ steps.packages.outputs.version }}"
|
||||
VERSION_NO_V="${VERSION#v}"
|
||||
echo "Updating packaging to version $VERSION_NO_V"
|
||||
|
||||
# Update openSUSE dms spec (stable only)
|
||||
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
|
||||
|
||||
# Update openSUSE spec changelog
|
||||
DATE_STR=$(date "+%a %b %d %Y")
|
||||
CHANGELOG_ENTRY="* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1\\n- Update to stable $VERSION release\\n- Bug fixes and improvements"
|
||||
sed -i "/%changelog/a\\$CHANGELOG_ENTRY\\n" distro/opensuse/dms.spec
|
||||
|
||||
# Update Debian _service files (both tar_scm and download_url formats)
|
||||
for service in distro/debian/*/_service; do
|
||||
if [[ -f "$service" ]]; then
|
||||
# Update tar_scm revision parameter (for dms-git)
|
||||
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
||||
|
||||
# Update download_url paths (for dms stable)
|
||||
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
|
||||
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
|
||||
fi
|
||||
done
|
||||
|
||||
# Update Debian changelog for dms stable
|
||||
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
|
||||
CHANGELOG_DATE=$(date -R)
|
||||
TEMP_CHANGELOG=$(mktemp)
|
||||
|
||||
cat > "$TEMP_CHANGELOG" << EOF
|
||||
dms ($VERSION_NO_V) stable; urgency=medium
|
||||
|
||||
* Update to $VERSION stable release
|
||||
* Bug fixes and improvements
|
||||
|
||||
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
|
||||
|
||||
EOF
|
||||
|
||||
cat "distro/debian/dms/debian/changelog" >> "$TEMP_CHANGELOG"
|
||||
mv "$TEMP_CHANGELOG" "distro/debian/dms/debian/changelog"
|
||||
|
||||
echo "✓ Updated Debian changelog to $VERSION_NO_V"
|
||||
fi
|
||||
|
||||
- name: Install Go
|
||||
if: steps.check-loop.outputs.skip != 'true'
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
|
||||
- name: Install OSC
|
||||
if: steps.check-loop.outputs.skip != 'true'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y osc
|
||||
|
||||
mkdir -p ~/.config/osc
|
||||
cat > ~/.config/osc/oscrc << EOF
|
||||
[general]
|
||||
apiurl = https://api.opensuse.org
|
||||
|
||||
[https://api.opensuse.org]
|
||||
user = ${{ secrets.OBS_USERNAME }}
|
||||
pass = ${{ secrets.OBS_PASSWORD }}
|
||||
EOF
|
||||
chmod 600 ~/.config/osc/oscrc
|
||||
|
||||
- name: Upload to OBS
|
||||
if: steps.check-loop.outputs.skip != 'true'
|
||||
env:
|
||||
FORCE_UPLOAD: ${{ github.event.inputs.force_upload }}
|
||||
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
||||
run: |
|
||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||
MESSAGE="Automated update from GitHub Actions"
|
||||
|
||||
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||
MESSAGE="Update to ${{ steps.packages.outputs.version }}"
|
||||
fi
|
||||
|
||||
if [[ "$PACKAGES" == "all" ]]; then
|
||||
bash distro/scripts/obs-upload.sh dms "$MESSAGE"
|
||||
bash distro/scripts/obs-upload.sh dms-git "Automated git update"
|
||||
else
|
||||
bash distro/scripts/obs-upload.sh "$PACKAGES" "$MESSAGE"
|
||||
fi
|
||||
|
||||
- name: Get changed packages
|
||||
if: steps.check-loop.outputs.skip != 'true'
|
||||
id: changed-packages
|
||||
run: |
|
||||
# Check if there are any changes to commit
|
||||
if git diff --exit-code distro/debian/ distro/opensuse/ >/dev/null 2>&1; then
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
echo "📋 No changelog or spec changes to commit"
|
||||
else
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
# Get list of changed packages for commit message
|
||||
CHANGED_DEB=$(git diff --name-only distro/debian/ 2>/dev/null | grep 'debian/changelog' | xargs dirname 2>/dev/null | xargs dirname 2>/dev/null | xargs basename 2>/dev/null | tr '\n' ', ' | sed 's/, $//' || echo "")
|
||||
CHANGED_SUSE=$(git diff --name-only distro/opensuse/ 2>/dev/null | grep '\.spec$' | sed 's|distro/opensuse/||' | sed 's/\.spec$//' | tr '\n' ', ' | sed 's/, $//' || echo "")
|
||||
|
||||
PKGS=$(echo "$CHANGED_DEB,$CHANGED_SUSE" | tr ',' '\n' | grep -v '^$' | sort -u | tr '\n' ',' | sed 's/,$//')
|
||||
echo "packages=$PKGS" >> $GITHUB_OUTPUT
|
||||
echo "📋 Changed packages: $PKGS"
|
||||
fi
|
||||
|
||||
- name: Commit packaging changes
|
||||
if: steps.check-loop.outputs.skip != 'true' && steps.changed-packages.outputs.has_changes == 'true'
|
||||
run: |
|
||||
git config user.name "dms-ci[bot]"
|
||||
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||
git add distro/debian/*/debian/changelog distro/opensuse/*.spec
|
||||
git commit -m "ci: Auto-update OBS packages [${{ steps.changed-packages.outputs.packages }}]" -m "🤖 Automated by GitHub Actions"
|
||||
git pull --rebase origin master
|
||||
git push
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "### OBS Package Update Complete" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
|
||||
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
|
||||
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "- **Project**: https://build.opensuse.org/project/show/home:AvengeMedia" >> $GITHUB_STEP_SUMMARY
|
||||
298
.github/workflows/backup/run-ppa.yml.bak
vendored
298
.github/workflows/backup/run-ppa.yml.bak
vendored
@@ -1,298 +0,0 @@
|
||||
name: Update PPA Packages
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: "Package to upload (dms, dms-git, dms-greeter, or all)"
|
||||
required: false
|
||||
default: "dms-git"
|
||||
force_upload:
|
||||
description: "Force upload without version check"
|
||||
required: false
|
||||
default: "false"
|
||||
type: choice
|
||||
options:
|
||||
- "false"
|
||||
- "true"
|
||||
rebuild_release:
|
||||
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
|
||||
required: false
|
||||
default: ""
|
||||
schedule:
|
||||
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
|
||||
|
||||
jobs:
|
||||
check-updates:
|
||||
name: Check for updates
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
has_updates: ${{ steps.check.outputs.has_updates }}
|
||||
packages: ${{ steps.check.outputs.packages }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check for updates
|
||||
id: check
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||
echo "Checking if dms-git source has changed..."
|
||||
|
||||
# Get current commit hash (8 chars to match changelog format)
|
||||
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||
|
||||
# Extract commit hash from changelog
|
||||
# Format: dms-git (0.6.2+git2264.c5c5ce84) questing; urgency=medium
|
||||
CHANGELOG_FILE="distro/ubuntu/dms-git/debian/changelog"
|
||||
|
||||
if [[ -f "$CHANGELOG_FILE" ]]; then
|
||||
CHANGELOG_COMMIT=$(head -1 "$CHANGELOG_FILE" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
||||
|
||||
if [[ -n "$CHANGELOG_COMMIT" ]]; then
|
||||
if [[ "$CURRENT_COMMIT" == "$CHANGELOG_COMMIT" ]]; then
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
echo "📋 Commit $CURRENT_COMMIT already in changelog, skipping upload"
|
||||
else
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "📋 New commit detected: $CURRENT_COMMIT (changelog has $CHANGELOG_COMMIT)"
|
||||
fi
|
||||
else
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "📋 Could not extract commit from changelog, proceeding with upload"
|
||||
fi
|
||||
else
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "📋 No changelog file found, proceeding with upload"
|
||||
fi
|
||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||
else
|
||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
upload-ppa:
|
||||
name: Upload to PPA
|
||||
needs: check-updates
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
if: |
|
||||
github.event.inputs.force_upload == 'true' ||
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
needs.check-updates.outputs.has_updates == 'true'
|
||||
|
||||
steps:
|
||||
- name: Generate GitHub App Token
|
||||
id: generate_token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
|
||||
- name: Check if last commit was automated
|
||||
id: check-loop
|
||||
run: |
|
||||
LAST_COMMIT_MSG=$(git log -1 --pretty=%B | head -1)
|
||||
if [[ "$LAST_COMMIT_MSG" == "ci: Auto-update PPA packages"* ]] || [[ "$LAST_COMMIT_MSG" == "ci: Auto-update OBS packages"* ]]; then
|
||||
echo "⏭️ Last commit was automated ($LAST_COMMIT_MSG), skipping to prevent infinite loop"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "✅ Last commit was not automated, proceeding"
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Set up Go
|
||||
if: steps.check-loop.outputs.skip != 'true'
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
cache: false
|
||||
|
||||
- name: Install build dependencies
|
||||
if: steps.check-loop.outputs.skip != 'true'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
debhelper \
|
||||
devscripts \
|
||||
dput \
|
||||
lftp \
|
||||
build-essential \
|
||||
fakeroot \
|
||||
dpkg-dev
|
||||
|
||||
- name: Configure GPG
|
||||
if: steps.check-loop.outputs.skip != 'true'
|
||||
env:
|
||||
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
run: |
|
||||
echo "$GPG_KEY" | gpg --import
|
||||
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
|
||||
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
|
||||
|
||||
- name: Determine packages to upload
|
||||
if: steps.check-loop.outputs.skip != 'true'
|
||||
id: packages
|
||||
run: |
|
||||
if [[ "${{ github.event.inputs.force_upload }}" == "true" ]]; then
|
||||
PKG="${{ github.event.inputs.package }}"
|
||||
if [[ -z "$PKG" || "$PKG" == "all" ]]; then
|
||||
echo "packages=all" >> $GITHUB_OUTPUT
|
||||
echo "🚀 Force upload: all packages"
|
||||
else
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "🚀 Force upload: $PKG"
|
||||
fi
|
||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
echo "Triggered by schedule: uploading git package"
|
||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||
# Manual package selection should respect change detection
|
||||
SELECTED_PKG="${{ github.event.inputs.package }}"
|
||||
UPDATED_PKG="${{ needs.check-updates.outputs.packages }}"
|
||||
|
||||
# Check if manually selected package is in the updated list
|
||||
if [[ "$UPDATED_PKG" == *"$SELECTED_PKG"* ]] || [[ "$SELECTED_PKG" == "all" ]]; then
|
||||
echo "packages=$SELECTED_PKG" >> $GITHUB_OUTPUT
|
||||
echo "📦 Manual selection (has updates): $SELECTED_PKG"
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "⚠️ Manual selection '$SELECTED_PKG' has no updates - skipping (use force_upload to override)"
|
||||
fi
|
||||
else
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Upload to PPA
|
||||
if: steps.check-loop.outputs.skip != 'true'
|
||||
run: |
|
||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||
REBUILD_RELEASE="${{ github.event.inputs.rebuild_release }}"
|
||||
|
||||
if [[ -z "$PACKAGES" ]]; then
|
||||
echo "No packages selected for upload. Skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Build command arguments
|
||||
BUILD_ARGS=()
|
||||
if [[ -n "$REBUILD_RELEASE" ]]; then
|
||||
BUILD_ARGS+=("$REBUILD_RELEASE")
|
||||
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||
fi
|
||||
|
||||
if [[ "$PACKAGES" == "all" ]]; then
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Uploading dms to PPA..."
|
||||
if [ -n "$REBUILD_RELEASE" ]; then
|
||||
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
bash distro/scripts/ppa-upload.sh dms dms questing "${BUILD_ARGS[@]}"
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Uploading dms-git to PPA..."
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
bash distro/scripts/ppa-upload.sh dms-git dms-git questing "${BUILD_ARGS[@]}"
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Uploading dms-greeter to PPA..."
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
bash distro/scripts/ppa-upload.sh dms-greeter danklinux questing "${BUILD_ARGS[@]}"
|
||||
else
|
||||
# Map package to PPA name
|
||||
case "$PACKAGES" in
|
||||
dms)
|
||||
PPA_NAME="dms"
|
||||
;;
|
||||
dms-git)
|
||||
PPA_NAME="dms-git"
|
||||
;;
|
||||
dms-greeter)
|
||||
PPA_NAME="danklinux"
|
||||
;;
|
||||
*)
|
||||
PPA_NAME="$PACKAGES"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Uploading $PACKAGES to PPA..."
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
bash distro/scripts/ppa-upload.sh "$PACKAGES" "$PPA_NAME" questing "${BUILD_ARGS[@]}"
|
||||
fi
|
||||
|
||||
- name: Get changed packages
|
||||
if: steps.check-loop.outputs.skip != 'true'
|
||||
id: changed-packages
|
||||
run: |
|
||||
# Check if there are any changelog changes to commit
|
||||
if git diff --exit-code distro/ubuntu/ >/dev/null 2>&1; then
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
echo "📋 No changelog changes to commit"
|
||||
else
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
# Get list of changed packages for commit message (deduplicate)
|
||||
CHANGED=$(git diff --name-only distro/ubuntu/ | grep 'debian/changelog' | sed 's|/debian/changelog||' | xargs -I{} basename {} | sort -u | tr '\n' ',' | sed 's/,$//')
|
||||
echo "packages=$CHANGED" >> $GITHUB_OUTPUT
|
||||
echo "📋 Changed packages: $CHANGED"
|
||||
echo "📋 Debug - Changed files:"
|
||||
git diff --name-only distro/ubuntu/ | grep 'debian/changelog' || echo "No changelog files found"
|
||||
fi
|
||||
|
||||
- name: Commit changelog changes
|
||||
if: steps.check-loop.outputs.skip != 'true' && steps.changed-packages.outputs.has_changes == 'true'
|
||||
run: |
|
||||
git config user.name "dms-ci[bot]"
|
||||
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||
git add distro/ubuntu/*/debian/changelog
|
||||
git commit -m "ci: Auto-update PPA packages [${{ steps.changed-packages.outputs.packages }}]" -m "🤖 Automated by GitHub Actions"
|
||||
git pull --rebase origin master
|
||||
git push
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
|
||||
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||
if [[ "$PACKAGES" == "all" ]]; then
|
||||
echo "- **PPA dms**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **PPA dms-git**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **PPA danklinux**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
|
||||
elif [[ "$PACKAGES" == "dms" ]]; then
|
||||
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
||||
elif [[ "$PACKAGES" == "dms-git" ]]; then
|
||||
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
|
||||
elif [[ "$PACKAGES" == "dms-greeter" ]]; then
|
||||
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
|
||||
245
.github/workflows/run-obs.yml
vendored
245
.github/workflows/run-obs.yml
vendored
@@ -10,6 +10,7 @@ on:
|
||||
options:
|
||||
- dms
|
||||
- dms-git
|
||||
- dms-greeter
|
||||
- all
|
||||
default: "dms"
|
||||
rebuild_release:
|
||||
@@ -72,12 +73,27 @@ jobs:
|
||||
fi
|
||||
}
|
||||
|
||||
# Helper function to check dms-greeter stable tag
|
||||
check_dms_greeter_stable() {
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:danklinux/dms-greeter/dms-greeter.spec" 2>/dev/null || echo "")
|
||||
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs | sed 's/^v//')
|
||||
|
||||
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "v$OBS_VERSION" ]]; then
|
||||
echo "📋 dms-greeter: Tag $LATEST_TAG already exists, skipping"
|
||||
return 1 # No update needed
|
||||
else
|
||||
echo "📋 dms-greeter: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
|
||||
return 0 # Update needed
|
||||
fi
|
||||
}
|
||||
|
||||
# Main logic
|
||||
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
# Tag selected or pushed - always update stable package
|
||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then
|
||||
# Run from tag with no package specified - update both stable packages
|
||||
echo "packages=dms dms-greeter" >> $GITHUB_OUTPUT
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
@@ -112,6 +128,10 @@ jobs:
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
if check_dms_greeter_stable; then
|
||||
PACKAGES_TO_UPDATE+=("dms-greeter")
|
||||
[[ -n "$LATEST_TAG" ]] && echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
||||
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
||||
@@ -144,6 +164,18 @@ jobs:
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms-greeter" ]]; then
|
||||
if check_dms_greeter_stable; then
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
else
|
||||
# Unknown package - proceed anyway
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
@@ -171,15 +203,8 @@ jobs:
|
||||
- name: Determine packages to update
|
||||
id: packages
|
||||
run: |
|
||||
# Check if GITHUB_REF points to a tag (works for both push events and workflow_dispatch with tag selected)
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
# Tag selected or pushed - use the tag from GITHUB_REF
|
||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Using tag from GITHUB_REF: $VERSION"
|
||||
# Check if check-updates already determined a version (from auto-detection)
|
||||
elif [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
|
||||
# Use check-updates outputs when available
|
||||
if [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
|
||||
# Use version from check-updates job
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
|
||||
@@ -191,40 +216,16 @@ jobs:
|
||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||
# Manual workflow dispatch
|
||||
|
||||
# Determine version for dms stable
|
||||
if [[ "${{ github.event.inputs.package }}" == "dms" ]]; then
|
||||
# Use github.ref if tag selected, otherwise auto-detect latest
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Using tag from GITHUB_REF: $VERSION"
|
||||
# Determine version for dms stable and dms-greeter using the API
|
||||
# GITHUB_REF is unreliable when "Use workflow from" a tag; API works from any ref
|
||||
if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "dms-greeter" ]] || [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
echo "Using latest release from API: $LATEST_TAG"
|
||||
else
|
||||
# Auto-detect latest release for dms
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
echo "Auto-detected latest release: $LATEST_TAG"
|
||||
else
|
||||
echo "ERROR: Could not auto-detect latest release"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
elif [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
||||
# Use github.ref if tag selected, otherwise auto-detect latest
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Using tag from GITHUB_REF: $VERSION"
|
||||
else
|
||||
# Auto-detect latest release for "all"
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
echo "Auto-detected latest release: $LATEST_TAG"
|
||||
else
|
||||
echo "ERROR: Could not auto-detect latest release"
|
||||
exit 1
|
||||
fi
|
||||
echo "ERROR: Could not fetch latest release from API"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -283,55 +284,66 @@ jobs:
|
||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
||||
} > "distro/debian/dms-git/debian/changelog"
|
||||
|
||||
- name: Update dms stable version
|
||||
- name: Update stable version (dms + dms-greeter)
|
||||
if: steps.packages.outputs.version != ''
|
||||
run: |
|
||||
VERSION="${{ steps.packages.outputs.version }}"
|
||||
VERSION_NO_V="${VERSION#v}"
|
||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||
echo "==> Updating packaging files to version: $VERSION_NO_V"
|
||||
|
||||
# Update spec file
|
||||
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
|
||||
# Update dms spec and changelog when dms is in the upload list
|
||||
if [[ "$PACKAGES" == *"dms"* ]]; then
|
||||
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
|
||||
UPDATED_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1)
|
||||
echo "✓ dms spec now shows Version: $UPDATED_VERSION"
|
||||
|
||||
# Verify the update
|
||||
UPDATED_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1)
|
||||
echo "✓ Spec file now shows Version: $UPDATED_VERSION"
|
||||
DATE_STR=$(date "+%a %b %d %Y")
|
||||
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms.spec)
|
||||
{
|
||||
echo "$LOCAL_SPEC_HEAD"
|
||||
echo "%changelog"
|
||||
echo "* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1"
|
||||
echo "- Update to stable $VERSION release"
|
||||
} > distro/opensuse/dms.spec
|
||||
|
||||
# Single changelog entry (full history on OBS website)
|
||||
DATE_STR=$(date "+%a %b %d %Y")
|
||||
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms.spec)
|
||||
{
|
||||
echo "$LOCAL_SPEC_HEAD"
|
||||
echo "%changelog"
|
||||
echo "* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1"
|
||||
echo "- Update to stable $VERSION release"
|
||||
} > distro/opensuse/dms.spec
|
||||
|
||||
# Update Debian _service files (both tar_scm and download_url formats)
|
||||
for service in distro/debian/*/_service; do
|
||||
if [[ -f "$service" ]]; then
|
||||
# Update tar_scm revision parameter (for dms-git)
|
||||
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
||||
|
||||
# Update download_url paths (for dms stable)
|
||||
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
|
||||
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
|
||||
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
|
||||
CHANGELOG_DATE=$(date -R)
|
||||
{
|
||||
echo "dms (${VERSION_NO_V}db1) stable; urgency=medium"
|
||||
echo ""
|
||||
echo " * Update to $VERSION stable release"
|
||||
echo ""
|
||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
||||
} > "distro/debian/dms/debian/changelog"
|
||||
echo "✓ Updated dms changelog to ${VERSION_NO_V}db1"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Update Debian changelog for dms stable (single entry, history on OBS website)
|
||||
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
|
||||
# Update dms-greeter changelog when dms-greeter is in the upload list
|
||||
if [[ "$PACKAGES" == *"dms-greeter"* ]] && [[ -f "distro/debian/dms-greeter/debian/changelog" ]]; then
|
||||
CHANGELOG_DATE=$(date -R)
|
||||
{
|
||||
echo "dms (${VERSION_NO_V}db1) stable; urgency=medium"
|
||||
echo "dms-greeter (${VERSION_NO_V}db1) unstable; urgency=medium"
|
||||
echo ""
|
||||
echo " * Update to $VERSION stable release"
|
||||
echo ""
|
||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
||||
} > "distro/debian/dms/debian/changelog"
|
||||
echo "✓ Updated Debian changelog to ${VERSION_NO_V}db1"
|
||||
} > "distro/debian/dms-greeter/debian/changelog"
|
||||
echo "✓ Updated dms-greeter changelog to ${VERSION_NO_V}db1"
|
||||
fi
|
||||
|
||||
# Update Debian _service files for packages in upload list (download_url paths)
|
||||
for service in distro/debian/*/_service; do
|
||||
if [[ -f "$service" ]]; then
|
||||
# Update tar_scm revision parameter (for dms-git)
|
||||
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
||||
# Update download_url paths (for dms, dms-greeter stable)
|
||||
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
|
||||
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
@@ -354,6 +366,7 @@ jobs:
|
||||
chmod 600 ~/.config/osc/oscrc
|
||||
|
||||
- name: Upload to OBS
|
||||
id: upload
|
||||
env:
|
||||
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
||||
TAG_VERSION: ${{ steps.packages.outputs.version }}
|
||||
@@ -362,6 +375,8 @@ jobs:
|
||||
|
||||
if [[ -z "$PACKAGES" ]]; then
|
||||
echo "✓ No packages need uploading. All up to date!"
|
||||
echo "uploaded_packages=" >> $GITHUB_OUTPUT
|
||||
echo "skipped_packages=" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -371,6 +386,9 @@ jobs:
|
||||
echo "==> Version being uploaded: ${{ steps.packages.outputs.version }}"
|
||||
fi
|
||||
|
||||
UPLOADED_PACKAGES=()
|
||||
SKIPPED_PACKAGES=()
|
||||
|
||||
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
|
||||
# Loop through each package and upload
|
||||
for PKG in $PACKAGES; do
|
||||
@@ -382,13 +400,37 @@ jobs:
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
LOG_FILE=$(mktemp)
|
||||
set +e
|
||||
if [[ "$PKG" == "dms-git" ]]; then
|
||||
bash distro/scripts/obs-upload.sh dms-git "Automated git update"
|
||||
bash distro/scripts/obs-upload.sh dms-git "Automated git update" >"$LOG_FILE" 2>&1
|
||||
else
|
||||
bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE"
|
||||
bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE" >"$LOG_FILE" 2>&1
|
||||
fi
|
||||
STATUS=$?
|
||||
set -e
|
||||
|
||||
cat "$LOG_FILE"
|
||||
|
||||
if [[ $STATUS -ne 0 ]]; then
|
||||
rm -f "$LOG_FILE"
|
||||
echo "❌ Upload failed for $PKG"
|
||||
exit $STATUS
|
||||
fi
|
||||
|
||||
if grep -Eq "Exiting gracefully \(no changes needed\)|No changes needed for this package\. Exiting gracefully\." "$LOG_FILE"; then
|
||||
echo "ℹ️ $PKG is already up to date. Skipped."
|
||||
SKIPPED_PACKAGES+=("$PKG")
|
||||
else
|
||||
UPLOADED_PACKAGES+=("$PKG")
|
||||
fi
|
||||
|
||||
rm -f "$LOG_FILE"
|
||||
done
|
||||
|
||||
echo "uploaded_packages=${UPLOADED_PACKAGES[*]}" >> $GITHUB_OUTPUT
|
||||
echo "skipped_packages=${SKIPPED_PACKAGES[*]}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
@@ -402,20 +444,59 @@ jobs:
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "All packages are current. Run completed successfully." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "**Packages Uploaded:**" >> $GITHUB_STEP_SUMMARY
|
||||
UPLOADED_PACKAGES="${{ steps.upload.outputs.uploaded_packages }}"
|
||||
SKIPPED_PACKAGES="${{ steps.upload.outputs.skipped_packages }}"
|
||||
TOTAL_COUNT=$(wc -w <<<"$PACKAGES" | tr -d ' ')
|
||||
UPLOADED_COUNT=0
|
||||
SKIPPED_COUNT=0
|
||||
if [[ -n "$UPLOADED_PACKAGES" ]]; then
|
||||
UPLOADED_COUNT=$(wc -w <<<"$UPLOADED_PACKAGES" | tr -d ' ')
|
||||
fi
|
||||
if [[ -n "$SKIPPED_PACKAGES" ]]; then
|
||||
SKIPPED_COUNT=$(wc -w <<<"$SKIPPED_PACKAGES" | tr -d ' ')
|
||||
fi
|
||||
in_list() {
|
||||
local item="$1"
|
||||
local list="$2"
|
||||
[[ " $list " == *" $item "* ]]
|
||||
}
|
||||
|
||||
if [[ "${{ job.status }}" == "success" ]]; then
|
||||
echo "**Status:** ✅ Completed successfully" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "**Status:** ❌ Completed with errors" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Processed:** $TOTAL_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Uploaded:** $UPLOADED_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Skipped (up to date):** $SKIPPED_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Packages:**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
for PKG in $PACKAGES; do
|
||||
STATUS_ICON="✅"
|
||||
STATUS_TEXT="uploaded"
|
||||
if in_list "$PKG" "$SKIPPED_PACKAGES"; then
|
||||
STATUS_ICON="ℹ️"
|
||||
STATUS_TEXT="up to date (skipped)"
|
||||
elif ! in_list "$PKG" "$UPLOADED_PACKAGES"; then
|
||||
STATUS_ICON="❌"
|
||||
STATUS_TEXT="failed"
|
||||
fi
|
||||
|
||||
case "$PKG" in
|
||||
dms)
|
||||
echo "- ✅ **dms** → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms/dms)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- $STATUS_ICON **dms** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms/dms)" >> $GITHUB_STEP_SUMMARY
|
||||
;;
|
||||
dms-git)
|
||||
echo "- ✅ **dms-git** → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- $STATUS_ICON **dms-git** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
|
||||
;;
|
||||
dms-greeter)
|
||||
echo "- $STATUS_ICON **dms-greeter** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:danklinux/dms-greeter)" >> $GITHUB_STEP_SUMMARY
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
|
||||
|
||||
@@ -5,11 +5,13 @@ repos:
|
||||
- id: trailing-whitespace
|
||||
- id: check-yaml
|
||||
- id: end-of-file-fixer
|
||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||
rev: v0.10.0.1
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
args: [-e, SC2164, -e, SC2001, -e, SC2012, -e, SC2317]
|
||||
name: shellcheck
|
||||
entry: shellcheck -e SC2164 -e SC2001 -e SC2012 -e SC2317
|
||||
language: system
|
||||
types: [shell]
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: go-mod-tidy
|
||||
|
||||
@@ -22,7 +22,7 @@ nix develop
|
||||
|
||||
This will provide:
|
||||
|
||||
- Go 1.24 toolchain (go, gopls, delve, go-tools) and GNU Make
|
||||
- Go 1.25+ toolchain (go, gopls, delve, go-tools) and GNU Make
|
||||
- Quickshell and required QML packages
|
||||
- Properly configured QML2_IMPORT_PATH
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ The on-screen preview displays the selected format. JSON output includes hex, RG
|
||||
|
||||
## Building
|
||||
|
||||
Requires Go 1.24+
|
||||
Requires Go 1.25+
|
||||
|
||||
**Development build:**
|
||||
|
||||
|
||||
@@ -525,5 +525,6 @@ func getCommonCommands() []*cobra.Command {
|
||||
doctorCmd,
|
||||
configCmd,
|
||||
dlCmd,
|
||||
randrCmd,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -649,58 +649,104 @@ func checkI2CAvailability() checkResult {
|
||||
return checkResult{catOptionalFeatures, "I2C/DDC", statusOK, fmt.Sprintf("%d monitor(s) detected", len(devices)), "External monitor brightness control", doctorDocsURL + "#optional-features"}
|
||||
}
|
||||
|
||||
func checkKImageFormats() checkResult {
|
||||
func checkImageFormatPlugins() []checkResult {
|
||||
url := doctorDocsURL + "#optional-features"
|
||||
desc := "Extra image format support (AVIF, HEIF, JXL)"
|
||||
|
||||
pluginDir := findQtPluginDir()
|
||||
if pluginDir == "" {
|
||||
return checkResult{catOptionalFeatures, "kimageformats", statusInfo, "Cannot detect (qtpaths not found)", desc, url}
|
||||
}
|
||||
|
||||
imageFormatsDir := filepath.Join(pluginDir, "imageformats")
|
||||
keyPlugins := []struct{ file, format string }{
|
||||
{"kimg_avif.so", "AVIF"},
|
||||
{"kimg_heif.so", "HEIF"},
|
||||
{"kimg_jxl.so", "JXL"},
|
||||
{"kimg_exr.so", "EXR"},
|
||||
}
|
||||
|
||||
var found []string
|
||||
for _, p := range keyPlugins {
|
||||
if _, err := os.Stat(filepath.Join(imageFormatsDir, p.file)); err == nil {
|
||||
found = append(found, p.format)
|
||||
pluginDirs := findQtPluginDirs()
|
||||
if len(pluginDirs) == 0 {
|
||||
return []checkResult{
|
||||
{catOptionalFeatures, "qt6-imageformats", statusInfo, "Cannot detect (plugin dir not found)", "WebP, TIFF, JP2 support", url},
|
||||
{catOptionalFeatures, "kimageformats", statusInfo, "Cannot detect (plugin dir not found)", "AVIF, HEIF, JXL support", url},
|
||||
}
|
||||
}
|
||||
|
||||
if len(found) == 0 {
|
||||
return checkResult{catOptionalFeatures, "kimageformats", statusWarn, "Not installed", desc, url}
|
||||
type pluginCheck struct {
|
||||
name string
|
||||
desc string
|
||||
plugins []struct{ file, format string }
|
||||
}
|
||||
|
||||
details := ""
|
||||
if doctorVerbose {
|
||||
details = fmt.Sprintf("Formats: %s (%s)", strings.Join(found, ", "), imageFormatsDir)
|
||||
checks := []pluginCheck{
|
||||
{
|
||||
name: "qt6-imageformats",
|
||||
desc: "WebP, TIFF, GIF, JP2 support",
|
||||
plugins: []struct{ file, format string }{
|
||||
{"libqwebp.so", "WebP"},
|
||||
{"libqtiff.so", "TIFF"},
|
||||
{"libqgif.so", "GIF"},
|
||||
{"libqjp2.so", "JP2"},
|
||||
{"libqicns.so", "ICNS"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "kimageformats",
|
||||
desc: "AVIF, HEIF, JXL support",
|
||||
plugins: []struct{ file, format string }{
|
||||
{"kimg_avif.so", "AVIF"},
|
||||
{"kimg_heif.so", "HEIF"},
|
||||
{"kimg_jxl.so", "JXL"},
|
||||
{"kimg_exr.so", "EXR"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return checkResult{catOptionalFeatures, "kimageformats", statusOK, fmt.Sprintf("Installed (%d formats)", len(found)), details, url}
|
||||
var results []checkResult
|
||||
for _, c := range checks {
|
||||
var found []string
|
||||
var foundDirs []string
|
||||
for _, pluginDir := range pluginDirs {
|
||||
imageFormatsDir := filepath.Join(pluginDir, "imageformats")
|
||||
for _, p := range c.plugins {
|
||||
if _, err := os.Stat(filepath.Join(imageFormatsDir, p.file)); err == nil {
|
||||
if !slices.Contains(found, p.format) {
|
||||
found = append(found, p.format)
|
||||
}
|
||||
if !slices.Contains(foundDirs, imageFormatsDir) {
|
||||
foundDirs = append(foundDirs, imageFormatsDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var result checkResult
|
||||
switch {
|
||||
case len(found) == 0:
|
||||
result = checkResult{catOptionalFeatures, c.name, statusWarn, "Not installed", c.desc, url}
|
||||
default:
|
||||
details := ""
|
||||
if doctorVerbose {
|
||||
details = fmt.Sprintf("Formats: %s (%s)", strings.Join(found, ", "), strings.Join(foundDirs, ":"))
|
||||
}
|
||||
result = checkResult{catOptionalFeatures, c.name, statusOK, fmt.Sprintf("Installed (%d formats)", len(found)), details, url}
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func findQtPluginDir() string {
|
||||
// Check QT_PLUGIN_PATH env var first (used by NixOS and custom setups)
|
||||
func findQtPluginDirs() []string {
|
||||
var dirs []string
|
||||
|
||||
addDir := func(dir string) {
|
||||
if dir != "" {
|
||||
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
|
||||
dirs = append(dirs, dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check all paths in QT_PLUGIN_PATH env var (used by NixOS and custom setups)
|
||||
if envPath := os.Getenv("QT_PLUGIN_PATH"); envPath != "" {
|
||||
for dir := range strings.SplitSeq(envPath, ":") {
|
||||
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
|
||||
return dir
|
||||
}
|
||||
addDir(dir)
|
||||
}
|
||||
}
|
||||
|
||||
// Try qtpaths
|
||||
for _, cmd := range []string{"qtpaths6", "qtpaths"} {
|
||||
if output, err := exec.Command(cmd, "-query", "QT_INSTALL_PLUGINS").Output(); err == nil {
|
||||
if dir := strings.TrimSpace(string(output)); dir != "" {
|
||||
return dir
|
||||
}
|
||||
addDir(strings.TrimSpace(string(output)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -711,12 +757,10 @@ func findQtPluginDir() string {
|
||||
"/usr/lib/x86_64-linux-gnu/qt6/plugins",
|
||||
"/usr/lib/aarch64-linux-gnu/qt6/plugins",
|
||||
} {
|
||||
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
|
||||
return dir
|
||||
}
|
||||
addDir(dir)
|
||||
}
|
||||
|
||||
return ""
|
||||
return dirs
|
||||
}
|
||||
|
||||
func detectNetworkBackend(stackResult *network.DetectResult) string {
|
||||
@@ -773,7 +817,7 @@ func checkOptionalDependencies() []checkResult {
|
||||
results = append(results, checkResult{catOptionalFeatures, "cups-pk-helper", cupsPkStatus, cupsPkMsg, "Printer management", optionalFeaturesURL})
|
||||
|
||||
results = append(results, checkI2CAvailability())
|
||||
results = append(results, checkKImageFormats())
|
||||
results = append(results, checkImageFormatPlugins()...)
|
||||
|
||||
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
||||
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
@@ -77,6 +78,28 @@ func installGreeter() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Debian/openSUSE
|
||||
greeter.TryInstallGreeterPackage(logFunc, "")
|
||||
if isPackageOnlyGreeterDistro() && !greeter.IsGreeterPackaged() {
|
||||
return fmt.Errorf("dms-greeter must be installed from distro packages on this distribution. %s", packageInstallHint())
|
||||
}
|
||||
if greeter.IsGreeterPackaged() && greeter.HasLegacyLocalGreeterWrapper() {
|
||||
return fmt.Errorf("legacy manual wrapper detected at /usr/local/bin/dms-greeter; remove it before using packaged dms-greeter: sudo rm -f /usr/local/bin/dms-greeter")
|
||||
}
|
||||
|
||||
// If already fully configured, prompt the user
|
||||
if isGreeterEnabled() {
|
||||
fmt.Print("\nGreeter is already installed and configured. Re-run to re-sync settings and permissions? [Y/n]: ")
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
response = strings.TrimSpace(strings.ToLower(response))
|
||||
if response == "n" || response == "no" {
|
||||
fmt.Println("Run 'dms greeter sync' to re-sync theme and settings at any time.")
|
||||
return nil
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
fmt.Println("\nDetecting DMS installation...")
|
||||
dmsPath, err := greeter.DetectDMSPath()
|
||||
if err != nil {
|
||||
@@ -114,7 +137,12 @@ func installGreeter() error {
|
||||
}
|
||||
|
||||
fmt.Println("\nConfiguring greetd...")
|
||||
if err := greeter.ConfigureGreetd(dmsPath, selectedCompositor, logFunc, ""); err != nil {
|
||||
// Use empty path when packaged (greeter finds /usr/share/quickshell/dms-greeter); else use user's DMS path
|
||||
greeterPathForConfig := ""
|
||||
if !greeter.IsGreeterPackaged() {
|
||||
greeterPathForConfig = dmsPath
|
||||
}
|
||||
if err := greeter.ConfigureGreetd(greeterPathForConfig, selectedCompositor, logFunc, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -123,11 +151,22 @@ func installGreeter() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ensureGraphicalTarget(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := handleConflictingDisplayManagers(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ensureGreetdEnabled(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("\n=== Installation Complete ===")
|
||||
fmt.Println("\nTo test the greeter, run:")
|
||||
fmt.Println("\nTo start the greeter now, run:")
|
||||
fmt.Println(" sudo systemctl start greetd")
|
||||
fmt.Println("\nTo enable on boot, run:")
|
||||
fmt.Println(" sudo systemctl enable --now greetd")
|
||||
fmt.Println("\nOr reboot to see the greeter at next boot.")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -327,20 +366,29 @@ func ensureGreetdEnabled() error {
|
||||
fmt.Println(" ✓ Unmasked greetd")
|
||||
}
|
||||
|
||||
switch state.EnabledState {
|
||||
case "disabled", "masked", "masked-runtime":
|
||||
if state.EnabledState == "enabled" || state.EnabledState == "enabled-runtime" {
|
||||
fmt.Println(" Reasserting greetd as active display manager...")
|
||||
} else {
|
||||
fmt.Println(" Enabling greetd service...")
|
||||
enableCmd := exec.Command("sudo", "systemctl", "enable", "greetd")
|
||||
enableCmd.Stdout = os.Stdout
|
||||
enableCmd.Stderr = os.Stderr
|
||||
if err := enableCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to enable greetd: %w", err)
|
||||
}
|
||||
|
||||
enableCmd := exec.Command("sudo", "systemctl", "enable", "--force", "greetd")
|
||||
enableCmd.Stdout = os.Stdout
|
||||
enableCmd.Stderr = os.Stderr
|
||||
if err := enableCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to enable greetd: %w", err)
|
||||
}
|
||||
|
||||
enabledState, _, verifyErr := checkSystemdServiceEnabled("greetd")
|
||||
if verifyErr != nil {
|
||||
fmt.Printf(" ⚠ Warning: Could not verify greetd enabled state: %v\n", verifyErr)
|
||||
} else {
|
||||
switch enabledState {
|
||||
case "enabled", "enabled-runtime", "static", "indirect", "alias":
|
||||
fmt.Printf(" ✓ greetd enabled (state: %s)\n", enabledState)
|
||||
default:
|
||||
return fmt.Errorf("greetd is still in state '%s' after enable operation", enabledState)
|
||||
}
|
||||
fmt.Println(" ✓ Enabled greetd service")
|
||||
case "enabled", "enabled-runtime":
|
||||
fmt.Println(" ✓ greetd is already enabled")
|
||||
default:
|
||||
fmt.Printf(" ℹ greetd is in state '%s' (should work, no action needed)\n", state.EnabledState)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -446,6 +494,10 @@ func enableGreeter() error {
|
||||
}
|
||||
|
||||
configContent := string(data)
|
||||
if greeter.IsGreeterPackaged() && greeter.HasLegacyLocalGreeterWrapper() {
|
||||
return fmt.Errorf("legacy manual wrapper detected at /usr/local/bin/dms-greeter; remove it before using packaged dms-greeter: sudo rm -f /usr/local/bin/dms-greeter")
|
||||
}
|
||||
|
||||
configAlreadyCorrect := strings.Contains(configContent, "dms-greeter")
|
||||
|
||||
if configAlreadyCorrect {
|
||||
@@ -611,6 +663,48 @@ func detectConfiguredCompositor() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func packageInstallHint() string {
|
||||
osInfo, err := distros.GetOSInfo()
|
||||
if err != nil {
|
||||
return "Install package: dms-greeter"
|
||||
}
|
||||
config, exists := distros.Registry[osInfo.Distribution.ID]
|
||||
if !exists {
|
||||
return "Install package: dms-greeter"
|
||||
}
|
||||
|
||||
switch config.Family {
|
||||
case distros.FamilyDebian:
|
||||
return "Install with 'sudo apt install dms-greeter' (requires DankLinux OBS repo — see https://danklinux.com/docs/dankgreeter/installation#debian)"
|
||||
case distros.FamilySUSE:
|
||||
return "Install with 'sudo zypper install dms-greeter' (requires DankLinux OBS repo — see https://danklinux.com/docs/dankgreeter/installation#opensuse)"
|
||||
case distros.FamilyUbuntu:
|
||||
return "Install with 'sudo apt install dms-greeter' (requires ppa:avengemedia/danklinux: sudo add-apt-repository ppa:avengemedia/danklinux)"
|
||||
case distros.FamilyFedora:
|
||||
return "Install with 'sudo dnf install dms-greeter' (requires COPR: sudo dnf copr enable avengemedia/danklinux)"
|
||||
case distros.FamilyArch:
|
||||
return "Install from AUR with 'paru -S greetd-dms-greeter-git' or 'yay -S greetd-dms-greeter-git'"
|
||||
default:
|
||||
return "Run 'dms greeter install' to install greeter"
|
||||
}
|
||||
}
|
||||
|
||||
func isPackageOnlyGreeterDistro() bool {
|
||||
osInfo, err := distros.GetOSInfo()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
config, exists := distros.Registry[osInfo.Distribution.ID]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
return config.Family == distros.FamilyDebian ||
|
||||
config.Family == distros.FamilySUSE ||
|
||||
config.Family == distros.FamilyUbuntu ||
|
||||
config.Family == distros.FamilyFedora ||
|
||||
config.Family == distros.FamilyArch
|
||||
}
|
||||
|
||||
func promptCompositorChoice(compositors []string) (string, error) {
|
||||
fmt.Println("\nMultiple compositors detected:")
|
||||
for i, comp := range compositors {
|
||||
@@ -679,7 +773,7 @@ func checkGreeterStatus() error {
|
||||
}
|
||||
} else {
|
||||
fmt.Println(" ✗ Greeter config not found")
|
||||
fmt.Println(" Run 'dms greeter install' to install greeter")
|
||||
fmt.Printf(" %s\n", packageInstallHint())
|
||||
}
|
||||
|
||||
fmt.Println("\nGroup Membership:")
|
||||
@@ -689,12 +783,13 @@ func checkGreeterStatus() error {
|
||||
return fmt.Errorf("failed to check groups: %w", err)
|
||||
}
|
||||
|
||||
inGreeterGroup := strings.Contains(string(groupsOutput), "greeter")
|
||||
greeterGroup := greeter.DetectGreeterGroup()
|
||||
inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup)
|
||||
if inGreeterGroup {
|
||||
fmt.Println(" ✓ User is in greeter group")
|
||||
fmt.Printf(" ✓ User is in %s group\n", greeterGroup)
|
||||
} else {
|
||||
fmt.Println(" ✗ User is NOT in greeter group")
|
||||
fmt.Println(" Run 'dms greeter install' to add user to greeter group")
|
||||
fmt.Printf(" ✗ User is NOT in %s group\n", greeterGroup)
|
||||
fmt.Println(" Run 'dms greeter sync' to set up group membership and permissions")
|
||||
}
|
||||
|
||||
cacheDir := "/var/cache/dms-greeter"
|
||||
@@ -703,7 +798,7 @@ func checkGreeterStatus() error {
|
||||
fmt.Printf(" ✓ %s exists\n", cacheDir)
|
||||
} else {
|
||||
fmt.Printf(" ✗ %s not found\n", cacheDir)
|
||||
fmt.Println(" Run 'dms greeter install' to create cache directory")
|
||||
fmt.Printf(" %s\n", packageInstallHint())
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
@@ -95,7 +97,11 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
||||
|
||||
func runMatugenGenerate(cmd *cobra.Command, args []string) {
|
||||
opts := buildMatugenOptions(cmd)
|
||||
if err := matugen.Run(opts); err != nil {
|
||||
err := matugen.Run(opts)
|
||||
switch {
|
||||
case errors.Is(err, matugen.ErrNoChanges):
|
||||
os.Exit(2)
|
||||
case err != nil:
|
||||
log.Fatalf("Theme generation failed: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -129,7 +135,11 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
||||
if !wait {
|
||||
if err := sendServerRequestFireAndForget(request); err != nil {
|
||||
log.Info("Server unavailable, running synchronously")
|
||||
if err := matugen.Run(opts); err != nil {
|
||||
err := matugen.Run(opts)
|
||||
switch {
|
||||
case errors.Is(err, matugen.ErrNoChanges):
|
||||
os.Exit(2)
|
||||
case err != nil:
|
||||
log.Fatalf("Theme generation failed: %v", err)
|
||||
}
|
||||
return
|
||||
@@ -146,11 +156,15 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
||||
resp, ok := tryServerRequest(request)
|
||||
if !ok {
|
||||
log.Info("Server unavailable, running synchronously")
|
||||
if err := matugen.Run(opts); err != nil {
|
||||
err := matugen.Run(opts)
|
||||
switch {
|
||||
case errors.Is(err, matugen.ErrNoChanges):
|
||||
resultCh <- matugen.ErrNoChanges
|
||||
case err != nil:
|
||||
resultCh <- err
|
||||
return
|
||||
default:
|
||||
resultCh <- nil
|
||||
}
|
||||
resultCh <- nil
|
||||
return
|
||||
}
|
||||
if resp.Error != "" {
|
||||
@@ -162,7 +176,10 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
||||
|
||||
select {
|
||||
case err := <-resultCh:
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, matugen.ErrNoChanges):
|
||||
os.Exit(2)
|
||||
case err != nil:
|
||||
log.Fatalf("Theme generation failed: %v", err)
|
||||
}
|
||||
fmt.Println("Theme generation completed")
|
||||
|
||||
58
core/cmd/dms/commands_randr.go
Normal file
58
core/cmd/dms/commands_randr.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var randrCmd = &cobra.Command{
|
||||
Use: "randr",
|
||||
Short: "Query output display information",
|
||||
Long: "Query Wayland compositor for output names, scales, resolutions and refresh rates via zwlr-output-management",
|
||||
Run: runRandr,
|
||||
}
|
||||
|
||||
func init() {
|
||||
randrCmd.Flags().Bool("json", false, "Output in JSON format")
|
||||
}
|
||||
|
||||
type randrJSON struct {
|
||||
Outputs []randrOutput `json:"outputs"`
|
||||
}
|
||||
|
||||
func runRandr(cmd *cobra.Command, args []string) {
|
||||
outputs, err := queryRandr()
|
||||
if err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
jsonFlag, _ := cmd.Flags().GetBool("json")
|
||||
|
||||
if jsonFlag {
|
||||
data, err := json.Marshal(randrJSON{Outputs: outputs})
|
||||
if err != nil {
|
||||
log.Fatalf("failed to marshal JSON: %v", err)
|
||||
}
|
||||
fmt.Println(string(data))
|
||||
return
|
||||
}
|
||||
|
||||
for i, out := range outputs {
|
||||
if i > 0 {
|
||||
fmt.Println()
|
||||
}
|
||||
status := "enabled"
|
||||
if !out.Enabled {
|
||||
status = "disabled"
|
||||
}
|
||||
fmt.Printf("%s (%s)\n", out.Name, status)
|
||||
fmt.Printf(" Scale: %.4g\n", out.Scale)
|
||||
fmt.Printf(" Resolution: %dx%d\n", out.Width, out.Height)
|
||||
if out.Refresh > 0 {
|
||||
fmt.Printf(" Refresh: %.2f Hz\n", float64(out.Refresh)/1000.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,16 +13,16 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ssOutputName string
|
||||
ssIncludeCursor bool
|
||||
ssFormat string
|
||||
ssQuality int
|
||||
ssOutputDir string
|
||||
ssFilename string
|
||||
ssNoClipboard bool
|
||||
ssNoFile bool
|
||||
ssNoNotify bool
|
||||
ssStdout bool
|
||||
ssOutputName string
|
||||
ssCursor string
|
||||
ssFormat string
|
||||
ssQuality int
|
||||
ssOutputDir string
|
||||
ssFilename string
|
||||
ssNoClipboard bool
|
||||
ssNoFile bool
|
||||
ssNoNotify bool
|
||||
ssStdout bool
|
||||
)
|
||||
|
||||
var screenshotCmd = &cobra.Command{
|
||||
@@ -52,7 +52,7 @@ Examples:
|
||||
dms screenshot last # Last region (pre-selected)
|
||||
dms screenshot --no-clipboard # Save file only
|
||||
dms screenshot --no-file # Clipboard only
|
||||
dms screenshot --cursor # Include cursor
|
||||
dms screenshot --cursor=on # Include cursor
|
||||
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ var notifyActionCmd = &cobra.Command{
|
||||
|
||||
func init() {
|
||||
screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode")
|
||||
screenshotCmd.PersistentFlags().BoolVar(&ssIncludeCursor, "cursor", false, "Include cursor in screenshot")
|
||||
screenshotCmd.PersistentFlags().StringVar(&ssCursor, "cursor", "off", "Include cursor in screenshot (on/off)")
|
||||
screenshotCmd.PersistentFlags().StringVarP(&ssFormat, "format", "f", "png", "Output format (png, jpg, ppm)")
|
||||
screenshotCmd.PersistentFlags().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)")
|
||||
screenshotCmd.PersistentFlags().StringVarP(&ssOutputDir, "dir", "d", "", "Output directory")
|
||||
@@ -136,7 +136,9 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
|
||||
config := screenshot.DefaultConfig()
|
||||
config.Mode = mode
|
||||
config.OutputName = ssOutputName
|
||||
config.IncludeCursor = ssIncludeCursor
|
||||
if strings.EqualFold(ssCursor, "on") {
|
||||
config.Cursor = screenshot.CursorOn
|
||||
}
|
||||
config.Clipboard = !ssNoClipboard
|
||||
config.SaveFile = !ssNoFile
|
||||
config.Notify = !ssNoNotify
|
||||
|
||||
@@ -18,7 +18,7 @@ func init() {
|
||||
runCmd.Flags().MarkHidden("daemon-child")
|
||||
|
||||
// Add subcommands to greeter
|
||||
greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
||||
|
||||
// Add subcommands to setup
|
||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||
|
||||
172
core/cmd/dms/randr_client.go
Normal file
172
core/cmd/dms/randr_client.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
|
||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
)
|
||||
|
||||
type randrOutput struct {
|
||||
Name string `json:"name"`
|
||||
Scale float64 `json:"scale"`
|
||||
Width int32 `json:"width"`
|
||||
Height int32 `json:"height"`
|
||||
Refresh int32 `json:"refresh"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type randrHead struct {
|
||||
name string
|
||||
enabled bool
|
||||
scale float64
|
||||
currentModeID uint32
|
||||
modeIDs []uint32
|
||||
}
|
||||
|
||||
type randrMode struct {
|
||||
width int32
|
||||
height int32
|
||||
refresh int32
|
||||
}
|
||||
|
||||
type randrClient struct {
|
||||
display *wlclient.Display
|
||||
ctx *wlclient.Context
|
||||
manager *wlr_output_management.ZwlrOutputManagerV1
|
||||
heads map[uint32]*randrHead
|
||||
modes map[uint32]*randrMode
|
||||
done bool
|
||||
err error
|
||||
}
|
||||
|
||||
func queryRandr() ([]randrOutput, error) {
|
||||
display, err := wlclient.Connect("")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
|
||||
}
|
||||
|
||||
c := &randrClient{
|
||||
display: display,
|
||||
ctx: display.Context(),
|
||||
heads: make(map[uint32]*randrHead),
|
||||
modes: make(map[uint32]*randrMode),
|
||||
}
|
||||
defer c.ctx.Close()
|
||||
|
||||
registry, err := display.GetRegistry()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get registry: %w", err)
|
||||
}
|
||||
|
||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
||||
if e.Interface == wlr_output_management.ZwlrOutputManagerV1InterfaceName {
|
||||
mgr := wlr_output_management.NewZwlrOutputManagerV1(c.ctx)
|
||||
version := min(e.Version, 4)
|
||||
|
||||
mgr.SetHeadHandler(func(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
|
||||
c.handleHead(e)
|
||||
})
|
||||
|
||||
mgr.SetDoneHandler(func(e wlr_output_management.ZwlrOutputManagerV1DoneEvent) {
|
||||
c.done = true
|
||||
})
|
||||
|
||||
if err := registry.Bind(e.Name, e.Interface, version, mgr); err == nil {
|
||||
c.manager = mgr
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// First roundtrip: discover globals and bind manager
|
||||
syncCallback, err := display.Sync()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sync display: %w", err)
|
||||
}
|
||||
syncCallback.SetDoneHandler(func(e wlclient.CallbackDoneEvent) {
|
||||
if c.manager == nil {
|
||||
c.err = fmt.Errorf("zwlr_output_manager_v1 protocol not supported by compositor")
|
||||
c.done = true
|
||||
}
|
||||
// Otherwise wait for manager's DoneHandler
|
||||
})
|
||||
|
||||
for !c.done {
|
||||
if err := c.ctx.Dispatch(); err != nil {
|
||||
return nil, fmt.Errorf("dispatch error: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if c.err != nil {
|
||||
return nil, c.err
|
||||
}
|
||||
|
||||
return c.buildOutputs(), nil
|
||||
}
|
||||
|
||||
func (c *randrClient) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
|
||||
handle := e.Head
|
||||
headID := handle.ID()
|
||||
|
||||
head := &randrHead{
|
||||
modeIDs: make([]uint32, 0),
|
||||
}
|
||||
c.heads[headID] = head
|
||||
|
||||
handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) {
|
||||
head.name = e.Name
|
||||
})
|
||||
|
||||
handle.SetEnabledHandler(func(e wlr_output_management.ZwlrOutputHeadV1EnabledEvent) {
|
||||
head.enabled = e.Enabled != 0
|
||||
})
|
||||
|
||||
handle.SetScaleHandler(func(e wlr_output_management.ZwlrOutputHeadV1ScaleEvent) {
|
||||
head.scale = e.Scale
|
||||
})
|
||||
|
||||
handle.SetCurrentModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1CurrentModeEvent) {
|
||||
head.currentModeID = e.Mode.ID()
|
||||
})
|
||||
|
||||
handle.SetModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1ModeEvent) {
|
||||
modeHandle := e.Mode
|
||||
modeID := modeHandle.ID()
|
||||
|
||||
head.modeIDs = append(head.modeIDs, modeID)
|
||||
|
||||
mode := &randrMode{}
|
||||
c.modes[modeID] = mode
|
||||
|
||||
modeHandle.SetSizeHandler(func(e wlr_output_management.ZwlrOutputModeV1SizeEvent) {
|
||||
mode.width = e.Width
|
||||
mode.height = e.Height
|
||||
})
|
||||
|
||||
modeHandle.SetRefreshHandler(func(e wlr_output_management.ZwlrOutputModeV1RefreshEvent) {
|
||||
mode.refresh = e.Refresh
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (c *randrClient) buildOutputs() []randrOutput {
|
||||
outputs := make([]randrOutput, 0, len(c.heads))
|
||||
|
||||
for _, head := range c.heads {
|
||||
out := randrOutput{
|
||||
Name: head.name,
|
||||
Scale: head.scale,
|
||||
Enabled: head.enabled,
|
||||
}
|
||||
|
||||
if mode, ok := c.modes[head.currentModeID]; ok {
|
||||
out.Width = mode.width
|
||||
out.Height = mode.height
|
||||
out.Refresh = mode.refresh
|
||||
}
|
||||
|
||||
outputs = append(outputs, out)
|
||||
}
|
||||
|
||||
return outputs
|
||||
}
|
||||
@@ -100,7 +100,7 @@ windowrule = float on, match:class ^(blueman-manager)$
|
||||
windowrule = float on, match:class ^(org\.gnome\.Nautilus)$
|
||||
windowrule = float on, match:class ^(xdg-desktop-portal)$
|
||||
|
||||
windowrule = noinitialfocus on, match:class ^(steam)$, match:title ^(notificationtoasts)
|
||||
windowrule = no_initial_focus on, match:class ^(steam)$, match:title ^(notificationtoasts)
|
||||
windowrule = pin on, match:class ^(steam)$, match:title ^(notificationtoasts)
|
||||
|
||||
windowrule = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture)$
|
||||
@@ -111,6 +111,7 @@ windowrule = float on, match:class ^(zoom)$
|
||||
# windowrule = float on, match:class ^(org.quickshell)$
|
||||
|
||||
layerrule = no_anim on, match:namespace ^(quickshell)$
|
||||
layerrule = no_anim on, match:namespace ^dms:.*
|
||||
|
||||
source = ./dms/colors.conf
|
||||
source = ./dms/outputs.conf
|
||||
|
||||
@@ -26,6 +26,9 @@ func init() {
|
||||
Register("cachyos", "#08A283", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewArchDistribution(config, logChan)
|
||||
})
|
||||
Register("catos", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewArchDistribution(config, logChan)
|
||||
})
|
||||
Register("endeavouros", "#7F3FBF", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewArchDistribution(config, logChan)
|
||||
})
|
||||
@@ -94,6 +97,7 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
|
||||
dependencies = append(dependencies, a.detectGit())
|
||||
dependencies = append(dependencies, a.detectWindowManager(wm))
|
||||
dependencies = append(dependencies, a.detectQuickshell())
|
||||
dependencies = append(dependencies, a.detectDMSGreeter())
|
||||
dependencies = append(dependencies, a.detectXDGPortal())
|
||||
dependencies = append(dependencies, a.detectAccountsService())
|
||||
|
||||
@@ -121,6 +125,10 @@ func (a *ArchDistribution) detectAccountsService() deps.Dependency {
|
||||
return a.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", a.packageInstalled("accountsservice"))
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) detectDMSGreeter() deps.Dependency {
|
||||
return a.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", a.packageInstalled("greetd-dms-greeter-git"))
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) packageInstalled(pkg string) bool {
|
||||
cmd := exec.Command("pacman", "-Q", pkg)
|
||||
err := cmd.Run()
|
||||
@@ -136,6 +144,7 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
|
||||
"dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]),
|
||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||
"quickshell": a.getQuickshellMapping(variants["quickshell"]),
|
||||
"dms-greeter": {Name: "greetd-dms-greeter-git", Repository: RepoTypeAUR},
|
||||
"matugen": a.getMatugenMapping(variants["matugen"]),
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeSystem},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
||||
|
||||
@@ -102,6 +102,19 @@ func (b *BaseDistribution) detectPackage(name, description string, installed boo
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectOptionalPackage(name, description string, installed bool) deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if installed {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
return deps.Dependency{
|
||||
Name: name,
|
||||
Status: status,
|
||||
Description: description,
|
||||
Required: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectGit() deps.Dependency {
|
||||
return b.detectCommand("git", "Version control system")
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
||||
dependencies = append(dependencies, d.detectGit())
|
||||
dependencies = append(dependencies, d.detectWindowManager(wm))
|
||||
dependencies = append(dependencies, d.detectQuickshell())
|
||||
dependencies = append(dependencies, d.detectDMSGreeter())
|
||||
dependencies = append(dependencies, d.detectXDGPortal())
|
||||
dependencies = append(dependencies, d.detectAccountsService())
|
||||
|
||||
@@ -86,6 +87,10 @@ func (d *DebianDistribution) detectAccountsService() deps.Dependency {
|
||||
return d.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", d.packageInstalled("accountsservice"))
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) detectDMSGreeter() deps.Dependency {
|
||||
return d.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", d.packageInstalled("dms-greeter"))
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
||||
cmd := exec.Command("dpkg", "-l", pkg)
|
||||
err := cmd.Run()
|
||||
@@ -108,6 +113,7 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
||||
// DMS packages from OBS with variant support
|
||||
"dms (DankMaterialShell)": d.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||
"quickshell": d.getQuickshellMapping(variants["quickshell"]),
|
||||
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
@@ -430,7 +436,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
||||
}
|
||||
|
||||
// Add repository
|
||||
repoLine := fmt.Sprintf("deb [signed-by=%s, arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL)
|
||||
repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
|
||||
@@ -13,6 +13,9 @@ func init() {
|
||||
Register("fedora", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewFedoraDistribution(config, logChan)
|
||||
})
|
||||
Register("evernight", "#72B8DC", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewFedoraDistribution(config, logChan)
|
||||
})
|
||||
Register("nobara", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewFedoraDistribution(config, logChan)
|
||||
})
|
||||
@@ -75,6 +78,7 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
||||
dependencies = append(dependencies, f.detectGit())
|
||||
dependencies = append(dependencies, f.detectWindowManager(wm))
|
||||
dependencies = append(dependencies, f.detectQuickshell())
|
||||
dependencies = append(dependencies, f.detectDMSGreeter())
|
||||
dependencies = append(dependencies, f.detectXDGPortal())
|
||||
dependencies = append(dependencies, f.detectAccountsService())
|
||||
|
||||
@@ -120,6 +124,7 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
||||
|
||||
// COPR packages
|
||||
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
|
||||
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||
@@ -191,6 +196,10 @@ func (f *FedoraDistribution) detectAccountsService() deps.Dependency {
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) detectDMSGreeter() deps.Dependency {
|
||||
return f.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", f.packageInstalled("dms-greeter"))
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) getPrerequisites() []string {
|
||||
return []string{
|
||||
"dnf-plugins-core",
|
||||
|
||||
@@ -55,6 +55,7 @@ const (
|
||||
PhaseAURPackages
|
||||
PhaseCursorTheme
|
||||
PhaseConfiguration
|
||||
PhaseGreeterSetup
|
||||
PhaseComplete
|
||||
)
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
|
||||
dependencies = append(dependencies, o.detectGit())
|
||||
dependencies = append(dependencies, o.detectWindowManager(wm))
|
||||
dependencies = append(dependencies, o.detectQuickshell())
|
||||
dependencies = append(dependencies, o.detectDMSGreeter())
|
||||
dependencies = append(dependencies, o.detectXDGPortal())
|
||||
dependencies = append(dependencies, o.detectAccountsService())
|
||||
|
||||
@@ -100,6 +101,10 @@ func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) detectDMSGreeter() deps.Dependency {
|
||||
return o.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", o.packageInstalled("dms-greeter"))
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||
return o.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||
}
|
||||
@@ -116,6 +121,7 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
||||
// DMS packages from OBS
|
||||
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||
"quickshell": o.getQuickshellMapping(variants["quickshell"]),
|
||||
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
|
||||
@@ -63,6 +63,7 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
||||
dependencies = append(dependencies, u.detectGit())
|
||||
dependencies = append(dependencies, u.detectWindowManager(wm))
|
||||
dependencies = append(dependencies, u.detectQuickshell())
|
||||
dependencies = append(dependencies, u.detectDMSGreeter())
|
||||
dependencies = append(dependencies, u.detectXDGPortal())
|
||||
dependencies = append(dependencies, u.detectAccountsService())
|
||||
|
||||
@@ -94,6 +95,10 @@ func (u *UbuntuDistribution) detectAccountsService() deps.Dependency {
|
||||
return u.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", u.packageInstalled("accountsservice"))
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) detectDMSGreeter() deps.Dependency {
|
||||
return u.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", u.packageInstalled("dms-greeter"))
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
|
||||
cmd := exec.Command("dpkg", "-l", pkg)
|
||||
err := cmd.Run()
|
||||
@@ -116,6 +121,7 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
||||
// DMS packages from PPAs
|
||||
"dms (DankMaterialShell)": u.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||
"quickshell": u.getQuickshellMapping(variants["quickshell"]),
|
||||
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/sblinch/kdl-go/document"
|
||||
)
|
||||
|
||||
// DetectDMSPath checks for DMS installation following XDG Base Directory specification
|
||||
func DetectDMSPath() (string, error) {
|
||||
return config.LocateDMSConfig()
|
||||
}
|
||||
@@ -37,7 +36,6 @@ func DetectGreeterGroup() string {
|
||||
return "greeter"
|
||||
}
|
||||
|
||||
// DetectCompositors checks which compositors are installed
|
||||
func DetectCompositors() []string {
|
||||
var compositors []string
|
||||
|
||||
@@ -51,7 +49,6 @@ func DetectCompositors() []string {
|
||||
return compositors
|
||||
}
|
||||
|
||||
// PromptCompositorChoice asks user to choose between compositors
|
||||
func PromptCompositorChoice(compositors []string) (string, error) {
|
||||
fmt.Println("\nMultiple compositors detected:")
|
||||
for i, comp := range compositors {
|
||||
@@ -79,9 +76,18 @@ func PromptCompositorChoice(compositors []string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// EnsureGreetdInstalled checks if greetd is installed and installs it if not
|
||||
// EnsureGreetdInstalled checks if greetd is installed - greetd is a daemon in /usr/sbin on Debian/Ubuntu
|
||||
func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
|
||||
if utils.CommandExists("greetd") {
|
||||
greetdFound := utils.CommandExists("greetd")
|
||||
if !greetdFound {
|
||||
for _, p := range []string{"/usr/sbin/greetd", "/sbin/greetd"} {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
greetdFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if greetdFound {
|
||||
logFunc("✓ greetd is already installed")
|
||||
return nil
|
||||
}
|
||||
@@ -142,6 +148,14 @@ func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "greetd")
|
||||
}
|
||||
|
||||
case distros.FamilyGentoo:
|
||||
if sudoPassword != "" {
|
||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
||||
"emerge --ask n sys-apps/greetd")
|
||||
} else {
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "emerge", "--ask", "n", "sys-apps/greetd")
|
||||
}
|
||||
|
||||
case distros.FamilyNix:
|
||||
return fmt.Errorf("on NixOS, please add greetd to your configuration.nix")
|
||||
|
||||
@@ -160,13 +174,124 @@ func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsGreeterPackaged returns true if dms-greeter was installed from a system package.
|
||||
func IsGreeterPackaged() bool {
|
||||
if !utils.CommandExists("dms-greeter") {
|
||||
return false
|
||||
}
|
||||
packagedPath := "/usr/share/quickshell/dms-greeter"
|
||||
info, err := os.Stat(packagedPath)
|
||||
return err == nil && info.IsDir()
|
||||
}
|
||||
|
||||
// HasLegacyLocalGreeterWrapper returns true when a manually installed wrapper exists.
|
||||
func HasLegacyLocalGreeterWrapper() bool {
|
||||
info, err := os.Stat("/usr/local/bin/dms-greeter")
|
||||
return err == nil && !info.IsDir()
|
||||
}
|
||||
|
||||
// TryInstallGreeterPackage attempts to install dms-greeter from the distro's official repo.
|
||||
func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool {
|
||||
osInfo, err := distros.GetOSInfo()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
config, exists := distros.Registry[osInfo.Distribution.ID]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
if IsGreeterPackaged() {
|
||||
logFunc("✓ dms-greeter package already installed")
|
||||
return true
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
var installCmd *exec.Cmd
|
||||
var failHint string
|
||||
|
||||
switch config.Family {
|
||||
case distros.FamilyDebian:
|
||||
obsSlug := getDebianOBSSlug(osInfo)
|
||||
keyURL := fmt.Sprintf("https://download.opensuse.org/repositories/home:AvengeMedia:danklinux/%s/Release.key", obsSlug)
|
||||
repoLine := fmt.Sprintf("deb [signed-by=/etc/apt/keyrings/danklinux.gpg] https://download.opensuse.org/repositories/home:/AvengeMedia:/danklinux/%s/ /", obsSlug)
|
||||
failHint = fmt.Sprintf("⚠ dms-greeter install failed. Add OBS repo manually:\ncurl -fsSL %s | sudo gpg --dearmor -o /etc/apt/keyrings/danklinux.gpg\necho '%s' | sudo tee /etc/apt/sources.list.d/danklinux.list\nsudo apt update && sudo apt install dms-greeter", keyURL, repoLine)
|
||||
logFunc(fmt.Sprintf("Adding DankLinux OBS repository (%s)...", obsSlug))
|
||||
addKeyCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||
fmt.Sprintf(`curl -fsSL %s | sudo gpg --dearmor -o /etc/apt/keyrings/danklinux.gpg`, keyURL))
|
||||
addKeyCmd.Stdout = os.Stdout
|
||||
addKeyCmd.Stderr = os.Stderr
|
||||
addKeyCmd.Run()
|
||||
addRepoCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||
fmt.Sprintf(`echo '%s' | sudo tee /etc/apt/sources.list.d/danklinux.list`, repoLine))
|
||||
addRepoCmd.Stdout = os.Stdout
|
||||
addRepoCmd.Stderr = os.Stderr
|
||||
addRepoCmd.Run()
|
||||
exec.CommandContext(ctx, "sudo", "apt-get", "update").Run()
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "dms-greeter")
|
||||
case distros.FamilySUSE:
|
||||
repoURL := getOpenSUSEOBSRepoURL(osInfo)
|
||||
failHint = fmt.Sprintf("⚠ dms-greeter install failed. Add OBS repo manually:\nsudo zypper addrepo %s\nsudo zypper refresh && sudo zypper install dms-greeter", repoURL)
|
||||
logFunc("Adding DankLinux OBS repository...")
|
||||
addRepoCmd := exec.CommandContext(ctx, "sudo", "zypper", "addrepo", repoURL)
|
||||
addRepoCmd.Stdout = os.Stdout
|
||||
addRepoCmd.Stderr = os.Stderr
|
||||
addRepoCmd.Run()
|
||||
exec.CommandContext(ctx, "sudo", "zypper", "refresh").Run()
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "zypper", "install", "-y", "dms-greeter")
|
||||
case distros.FamilyUbuntu:
|
||||
failHint = "⚠ dms-greeter install failed. Add PPA manually: sudo add-apt-repository ppa:avengemedia/danklinux && sudo apt-get update && sudo apt-get install dms-greeter"
|
||||
logFunc("Enabling PPA ppa:avengemedia/danklinux...")
|
||||
ppacmd := exec.CommandContext(ctx, "sudo", "add-apt-repository", "-y", "ppa:avengemedia/danklinux")
|
||||
ppacmd.Stdout = os.Stdout
|
||||
ppacmd.Stderr = os.Stderr
|
||||
ppacmd.Run()
|
||||
exec.CommandContext(ctx, "sudo", "apt-get", "update").Run()
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "dms-greeter")
|
||||
case distros.FamilyFedora:
|
||||
failHint = "⚠ dms-greeter install failed. Enable COPR manually: sudo dnf copr enable avengemedia/danklinux && sudo dnf install dms-greeter"
|
||||
logFunc("Enabling COPR avengemedia/danklinux...")
|
||||
coprcmd := exec.CommandContext(ctx, "sudo", "dnf", "copr", "enable", "-y", "avengemedia/danklinux")
|
||||
coprcmd.Stdout = os.Stdout
|
||||
coprcmd.Stderr = os.Stderr
|
||||
coprcmd.Run()
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "dnf", "install", "-y", "dms-greeter")
|
||||
case distros.FamilyArch:
|
||||
aurHelper := ""
|
||||
for _, helper := range []string{"paru", "yay"} {
|
||||
if _, err := exec.LookPath(helper); err == nil {
|
||||
aurHelper = helper
|
||||
break
|
||||
}
|
||||
}
|
||||
if aurHelper == "" {
|
||||
logFunc("⚠ No AUR helper found (paru/yay). Install greetd-dms-greeter-git from AUR: https://aur.archlinux.org/packages/greetd-dms-greeter-git")
|
||||
return false
|
||||
}
|
||||
failHint = fmt.Sprintf("⚠ dms-greeter install failed. Install from AUR: %s -S greetd-dms-greeter-git", aurHelper)
|
||||
installCmd = exec.CommandContext(ctx, aurHelper, "-S", "--noconfirm", "greetd-dms-greeter-git")
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
logFunc("Installing dms-greeter from official repository...")
|
||||
installCmd.Stdout = os.Stdout
|
||||
installCmd.Stderr = os.Stderr
|
||||
|
||||
if err := installCmd.Run(); err != nil {
|
||||
logFunc(failHint)
|
||||
return false
|
||||
}
|
||||
|
||||
logFunc("✓ dms-greeter package installed")
|
||||
return true
|
||||
}
|
||||
|
||||
// CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory
|
||||
func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
||||
// Check if dms-greeter is already in PATH
|
||||
if utils.CommandExists("dms-greeter") {
|
||||
logFunc("✓ dms-greeter wrapper already installed")
|
||||
} else {
|
||||
// Install the wrapper script
|
||||
assetsDir := filepath.Join(dmsPath, "Modules", "Greetd", "assets")
|
||||
wrapperSrc := filepath.Join(assetsDir, "dms-greeter")
|
||||
|
||||
@@ -203,7 +328,6 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
|
||||
}
|
||||
}
|
||||
|
||||
// Create cache directory with proper permissions
|
||||
cacheDir := "/var/cache/dms-greeter"
|
||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", cacheDir); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
@@ -224,14 +348,90 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureACLInstalled installs the acl package (setfacl/getfacl) if not already present
|
||||
func EnsureACLInstalled(logFunc func(string), sudoPassword string) error {
|
||||
if utils.CommandExists("setfacl") {
|
||||
return nil
|
||||
}
|
||||
|
||||
logFunc("setfacl not found – installing acl package...")
|
||||
|
||||
osInfo, err := distros.GetOSInfo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to detect OS: %w", err)
|
||||
}
|
||||
|
||||
config, exists := distros.Registry[osInfo.Distribution.ID]
|
||||
if !exists {
|
||||
return fmt.Errorf("unsupported distribution for automatic acl installation: %s", osInfo.Distribution.ID)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
var installCmd *exec.Cmd
|
||||
|
||||
switch config.Family {
|
||||
case distros.FamilyArch:
|
||||
if sudoPassword != "" {
|
||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm acl")
|
||||
} else {
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "pacman", "-S", "--needed", "--noconfirm", "acl")
|
||||
}
|
||||
|
||||
case distros.FamilyFedora:
|
||||
if sudoPassword != "" {
|
||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "dnf install -y acl")
|
||||
} else {
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "dnf", "install", "-y", "acl")
|
||||
}
|
||||
|
||||
case distros.FamilySUSE:
|
||||
if sudoPassword != "" {
|
||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "zypper install -y acl")
|
||||
} else {
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "zypper", "install", "-y", "acl")
|
||||
}
|
||||
|
||||
case distros.FamilyUbuntu, distros.FamilyDebian:
|
||||
if sudoPassword != "" {
|
||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "apt-get install -y acl")
|
||||
} else {
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "acl")
|
||||
}
|
||||
|
||||
case distros.FamilyGentoo:
|
||||
if sudoPassword != "" {
|
||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "emerge --ask n sys-fs/acl")
|
||||
} else {
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "emerge", "--ask", "n", "sys-fs/acl")
|
||||
}
|
||||
|
||||
case distros.FamilyNix:
|
||||
return fmt.Errorf("on NixOS, please add pkgs.acl to your configuration.nix")
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported distribution family for automatic acl installation: %s", config.Family)
|
||||
}
|
||||
|
||||
installCmd.Stdout = os.Stdout
|
||||
installCmd.Stderr = os.Stderr
|
||||
if err := installCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install acl: %w", err)
|
||||
}
|
||||
|
||||
logFunc("✓ acl package installed")
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetupParentDirectoryACLs sets ACLs on parent directories to allow traversal
|
||||
func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
|
||||
if err := EnsureACLInstalled(logFunc, sudoPassword); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: could not install acl package: %v", err))
|
||||
logFunc(" ACL permissions will be skipped; theme sync may not work correctly.")
|
||||
return nil
|
||||
}
|
||||
if !utils.CommandExists("setfacl") {
|
||||
logFunc("⚠ Warning: setfacl command not found. ACL support may not be available on this filesystem.")
|
||||
logFunc(" If theme sync doesn't work, you may need to install acl package:")
|
||||
logFunc(" - Fedora/RHEL: sudo dnf install acl")
|
||||
logFunc(" - Debian/Ubuntu: sudo apt-get install acl")
|
||||
logFunc(" - Arch: sudo pacman -S acl")
|
||||
// setfacl still not found after install attempt (e.g. unsupported filesystem)
|
||||
logFunc("⚠ Warning: setfacl still not available after install attempt; skipping ACL setup.")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -264,7 +464,6 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Set ACL to allow greeter user read+execute permission (for session discovery)
|
||||
if err := runSudoCmd(sudoPassword, "setfacl", "-m", fmt.Sprintf("u:%s:rx", owner), dir.path); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err))
|
||||
logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m u:%s:x %s", owner, dir.path))
|
||||
@@ -299,7 +498,6 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
||||
if err == nil && strings.Contains(string(groupsOutput), group) {
|
||||
logFunc(fmt.Sprintf("✓ %s is already in %s group", currentUser, group))
|
||||
} else {
|
||||
// Add current user to greeter group for file access permissions
|
||||
if err := runSudoCmd(sudoPassword, "usermod", "-aG", group, currentUser); err != nil {
|
||||
return fmt.Errorf("failed to add %s to %s group: %w", currentUser, group, err)
|
||||
}
|
||||
@@ -339,7 +537,6 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
||||
logFunc(fmt.Sprintf("✓ Set group permissions for %s", dir.desc))
|
||||
}
|
||||
|
||||
// Set up ACLs on parent directories to allow greeter user traversal
|
||||
if err := SetupParentDirectoryACLs(logFunc, sudoPassword); err != nil {
|
||||
return fmt.Errorf("failed to setup parent directory ACLs: %w", err)
|
||||
}
|
||||
@@ -393,7 +590,7 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
|
||||
}
|
||||
}
|
||||
|
||||
runSudoCmd(sudoPassword, "rm", "-f", link.target) //nolint:errcheck
|
||||
_ = runSudoCmd(sudoPassword, "rm", "-f", link.target)
|
||||
|
||||
if err := runSudoCmd(sudoPassword, "ln", "-sf", link.source, link.target); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to create symlink for %s: %v", link.desc, err))
|
||||
@@ -787,15 +984,19 @@ user = "%s"
|
||||
}
|
||||
}
|
||||
|
||||
// Determine wrapper command path
|
||||
// If dmsPath is empty (packaged greeter), omit -p; wrapper finds /usr/share/quickshell/dms-greeter
|
||||
wrapperCmd := "dms-greeter"
|
||||
if !utils.CommandExists("dms-greeter") {
|
||||
wrapperCmd = "/usr/local/bin/dms-greeter"
|
||||
}
|
||||
|
||||
// Build command based on compositor and dms path
|
||||
compositorLower := strings.ToLower(compositor)
|
||||
command := fmt.Sprintf(`command = "%s --command %s -p %s"`, wrapperCmd, compositorLower, dmsPath)
|
||||
var command string
|
||||
if dmsPath == "" {
|
||||
command = fmt.Sprintf(`command = "%s --command %s"`, wrapperCmd, compositorLower)
|
||||
} else {
|
||||
command = fmt.Sprintf(`command = "%s --command %s -p %s"`, wrapperCmd, compositorLower, dmsPath)
|
||||
}
|
||||
|
||||
var finalLines []string
|
||||
inDefaultSession := false
|
||||
@@ -832,7 +1033,11 @@ user = "%s"
|
||||
return fmt.Errorf("failed to move config to /etc/greetd: %w", err)
|
||||
}
|
||||
|
||||
logFunc(fmt.Sprintf("✓ Updated greetd configuration (user: %s, command: %s --command %s -p %s)", greeterUser, wrapperCmd, compositorLower, dmsPath))
|
||||
cmdDesc := fmt.Sprintf("%s --command %s", wrapperCmd, compositorLower)
|
||||
if dmsPath != "" {
|
||||
cmdDesc = fmt.Sprintf("%s -p %s", cmdDesc, dmsPath)
|
||||
}
|
||||
logFunc(fmt.Sprintf("✓ Updated greetd configuration (user: %s, command: %s)", greeterUser, cmdDesc))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -867,6 +1072,47 @@ func stripConfigFlag(command string) string {
|
||||
return command
|
||||
}
|
||||
|
||||
// getDebianOBSSlug returns the OBS repository slug for the running Debian version.
|
||||
func getDebianOBSSlug(osInfo *distros.OSInfo) string {
|
||||
versionID := strings.ToLower(osInfo.VersionID)
|
||||
codename := strings.ToLower(osInfo.VersionCodename)
|
||||
prettyName := strings.ToLower(osInfo.PrettyName)
|
||||
|
||||
if strings.Contains(prettyName, "sid") || strings.Contains(prettyName, "unstable") ||
|
||||
codename == "sid" || versionID == "sid" {
|
||||
return "Debian_Unstable"
|
||||
}
|
||||
if versionID == "testing" || codename == "testing" {
|
||||
return "Debian_Testing"
|
||||
}
|
||||
if versionID != "" {
|
||||
return "Debian_" + versionID // "Debian_13"
|
||||
}
|
||||
return "Debian_Unstable"
|
||||
}
|
||||
|
||||
// getOpenSUSEOBSRepoURL returns the OBS .repo file URL for the running openSUSE variant.
|
||||
func getOpenSUSEOBSRepoURL(osInfo *distros.OSInfo) string {
|
||||
const base = "https://download.opensuse.org/repositories/home:AvengeMedia:danklinux"
|
||||
var slug string
|
||||
switch osInfo.Distribution.ID {
|
||||
case "opensuse-leap":
|
||||
v := osInfo.VersionID
|
||||
if v != "" && !strings.Contains(v, ".") {
|
||||
v += ".0" // "16" → "16.0"
|
||||
}
|
||||
if v == "" {
|
||||
v = "16.0"
|
||||
}
|
||||
slug = v
|
||||
case "opensuse-slowroll":
|
||||
slug = "openSUSE_Slowroll"
|
||||
default: // opensuse-tumbleweed || unknown version
|
||||
slug = "openSUSE_Tumbleweed"
|
||||
}
|
||||
return fmt.Sprintf("%s/%s/home:AvengeMedia:danklinux.repo", base, slug)
|
||||
}
|
||||
|
||||
func runSudoCmd(sudoPassword string, command string, args ...string) error {
|
||||
var cmd *exec.Cmd
|
||||
|
||||
@@ -887,3 +1133,140 @@ func runSudoCmd(sudoPassword string, command string, args ...string) error {
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func checkSystemdEnabled(service string) (string, error) {
|
||||
cmd := exec.Command("systemctl", "is-enabled", service)
|
||||
output, _ := cmd.Output()
|
||||
return strings.TrimSpace(string(output)), nil
|
||||
}
|
||||
|
||||
func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)) error {
|
||||
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
|
||||
for _, dm := range conflictingDMs {
|
||||
state, err := checkSystemdEnabled(dm)
|
||||
if err != nil || state == "" || state == "not-found" {
|
||||
continue
|
||||
}
|
||||
switch state {
|
||||
case "enabled", "enabled-runtime", "static", "indirect", "alias":
|
||||
logFunc(fmt.Sprintf("Disabling conflicting display manager: %s", dm))
|
||||
if err := runSudoCmd(sudoPassword, "systemctl", "disable", "--now", dm); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to disable %s: %v", dm, err))
|
||||
} else {
|
||||
logFunc(fmt.Sprintf("✓ Disabled %s", dm))
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnableGreetd unmasks and enables greetd, forcing it over any other DM.
|
||||
func EnableGreetd(sudoPassword string, logFunc func(string)) error {
|
||||
state, err := checkSystemdEnabled("greetd")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check greetd state: %w", err)
|
||||
}
|
||||
if state == "not-found" {
|
||||
return fmt.Errorf("greetd service not found; ensure greetd is installed")
|
||||
}
|
||||
if state == "masked" || state == "masked-runtime" {
|
||||
logFunc(" Unmasking greetd...")
|
||||
if err := runSudoCmd(sudoPassword, "systemctl", "unmask", "greetd"); err != nil {
|
||||
return fmt.Errorf("failed to unmask greetd: %w", err)
|
||||
}
|
||||
logFunc(" ✓ Unmasked greetd")
|
||||
}
|
||||
logFunc(" Enabling greetd service (--force)...")
|
||||
if err := runSudoCmd(sudoPassword, "systemctl", "enable", "--force", "greetd"); err != nil {
|
||||
return fmt.Errorf("failed to enable greetd: %w", err)
|
||||
}
|
||||
logFunc("✓ greetd enabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnsureGraphicalTarget(sudoPassword string, logFunc func(string)) error {
|
||||
cmd := exec.Command("systemctl", "get-default")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: could not get default systemd target: %v", err))
|
||||
return nil
|
||||
}
|
||||
current := strings.TrimSpace(string(output))
|
||||
if current == "graphical.target" {
|
||||
logFunc("✓ Default target is already graphical.target")
|
||||
return nil
|
||||
}
|
||||
logFunc(fmt.Sprintf(" Setting default target to graphical.target (was: %s)...", current))
|
||||
if err := runSudoCmd(sudoPassword, "systemctl", "set-default", "graphical.target"); err != nil {
|
||||
return fmt.Errorf("failed to set graphical target: %w", err)
|
||||
}
|
||||
logFunc("✓ Default target set to graphical.target")
|
||||
return nil
|
||||
}
|
||||
|
||||
// AutoSetupGreeter performs the full non-interactive greeter setup
|
||||
func AutoSetupGreeter(compositor, sudoPassword string, logFunc func(string)) error {
|
||||
if IsGreeterPackaged() && HasLegacyLocalGreeterWrapper() {
|
||||
return fmt.Errorf("legacy manual wrapper detected at /usr/local/bin/dms-greeter; " +
|
||||
"remove it before using packaged dms-greeter: sudo rm -f /usr/local/bin/dms-greeter")
|
||||
}
|
||||
|
||||
logFunc("Ensuring greetd is installed...")
|
||||
if err := EnsureGreetdInstalled(logFunc, sudoPassword); err != nil {
|
||||
return fmt.Errorf("greetd install failed: %w", err)
|
||||
}
|
||||
|
||||
dmsPath := ""
|
||||
if !IsGreeterPackaged() {
|
||||
detected, err := DetectDMSPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("DMS installation not found: %w", err)
|
||||
}
|
||||
dmsPath = detected
|
||||
logFunc(fmt.Sprintf("✓ Found DMS at: %s", dmsPath))
|
||||
} else {
|
||||
logFunc("✓ Using packaged dms-greeter (/usr/share/quickshell/dms-greeter)")
|
||||
}
|
||||
|
||||
logFunc("Setting up dms-greeter group and permissions...")
|
||||
if err := SetupDMSGroup(logFunc, sudoPassword); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: group/permissions setup error: %v", err))
|
||||
}
|
||||
|
||||
logFunc("Copying greeter files...")
|
||||
if err := CopyGreeterFiles(dmsPath, compositor, logFunc, sudoPassword); err != nil {
|
||||
return fmt.Errorf("failed to copy greeter files: %w", err)
|
||||
}
|
||||
|
||||
logFunc("Configuring greetd...")
|
||||
greeterPathForConfig := ""
|
||||
if !IsGreeterPackaged() {
|
||||
greeterPathForConfig = dmsPath
|
||||
}
|
||||
if err := ConfigureGreetd(greeterPathForConfig, compositor, logFunc, sudoPassword); err != nil {
|
||||
return fmt.Errorf("failed to configure greetd: %w", err)
|
||||
}
|
||||
|
||||
logFunc("Synchronizing DMS configurations...")
|
||||
if err := SyncDMSConfigs(dmsPath, compositor, logFunc, sudoPassword); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: config sync error: %v", err))
|
||||
}
|
||||
|
||||
logFunc("Checking for conflicting display managers...")
|
||||
if err := DisableConflictingDisplayManagers(sudoPassword, logFunc); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: %v", err))
|
||||
}
|
||||
|
||||
logFunc("Enabling greetd service...")
|
||||
if err := EnableGreetd(sudoPassword, logFunc); err != nil {
|
||||
return fmt.Errorf("failed to enable greetd: %w", err)
|
||||
}
|
||||
|
||||
logFunc("Ensuring graphical.target as default...")
|
||||
if err := EnsureGraphicalTarget(sudoPassword, logFunc); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: %v", err))
|
||||
}
|
||||
|
||||
logFunc("✓ DMS greeter setup complete")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -341,6 +341,8 @@ func (n *NiriProvider) buildActionFromNode(bindNode *document.Node) string {
|
||||
val := arg.ValueString()
|
||||
if val == "" {
|
||||
parts = append(parts, `""`)
|
||||
} else if strings.ContainsAny(val, " \t") {
|
||||
parts = append(parts, `"`+strings.ReplaceAll(val, `"`, `\"`)+`"`)
|
||||
} else {
|
||||
parts = append(parts, val)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package matugen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
@@ -19,6 +21,8 @@ import (
|
||||
"github.com/lucasb-eyer/go-colorful"
|
||||
)
|
||||
|
||||
var ErrNoChanges = errors.New("no color changes")
|
||||
|
||||
type ColorMode string
|
||||
|
||||
const (
|
||||
@@ -33,6 +37,7 @@ const (
|
||||
TemplateKindTerminal
|
||||
TemplateKindGTK
|
||||
TemplateKindVSCode
|
||||
TemplateKindEmacs
|
||||
)
|
||||
|
||||
type TemplateDef struct {
|
||||
@@ -53,7 +58,7 @@ var templateRegistry = []TemplateDef{
|
||||
{ID: "qt6ct", Commands: []string{"qt6ct"}, ConfigFile: "qt6ct.toml"},
|
||||
{ID: "firefox", Commands: []string{"firefox"}, ConfigFile: "firefox.toml"},
|
||||
{ID: "pywalfox", Commands: []string{"pywalfox"}, ConfigFile: "pywalfox.toml"},
|
||||
{ID: "zenbrowser", Commands: []string{"zen", "zen-browser"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"},
|
||||
{ID: "zenbrowser", Commands: []string{"zen", "zen-browser", "zen-beta", "zen-twilight"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"},
|
||||
{ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigFile: "vesktop.toml"},
|
||||
{ID: "equibop", Commands: []string{"equibop"}, ConfigFile: "equibop.toml"},
|
||||
{ID: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal},
|
||||
@@ -65,7 +70,7 @@ var templateRegistry = []TemplateDef{
|
||||
{ID: "dgop", Commands: []string{"dgop"}, ConfigFile: "dgop.toml"},
|
||||
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
|
||||
{ID: "vscode", Kind: TemplateKindVSCode},
|
||||
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml"},
|
||||
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs},
|
||||
}
|
||||
|
||||
func (c *ColorMode) GTKTheme() string {
|
||||
@@ -78,7 +83,8 @@ func (c *ColorMode) GTKTheme() string {
|
||||
}
|
||||
|
||||
var (
|
||||
matugenVersionOnce sync.Once
|
||||
matugenVersionMu sync.Mutex
|
||||
matugenVersionOK bool
|
||||
matugenSupportsCOE bool
|
||||
matugenIsV4 bool
|
||||
)
|
||||
@@ -158,8 +164,14 @@ func Run(opts Options) error {
|
||||
|
||||
log.Infof("Building theme: %s %s (%s)", opts.Kind, opts.Value, opts.Mode)
|
||||
|
||||
if err := buildOnce(&opts); err != nil {
|
||||
return err
|
||||
changed, buildErr := buildOnce(&opts)
|
||||
if buildErr != nil {
|
||||
return buildErr
|
||||
}
|
||||
|
||||
if !changed {
|
||||
log.Info("No color changes detected, skipping refresh")
|
||||
return ErrNoChanges
|
||||
}
|
||||
|
||||
if opts.SyncModeWithPortal {
|
||||
@@ -170,25 +182,27 @@ func Run(opts Options) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildOnce(opts *Options) error {
|
||||
func buildOnce(opts *Options) (bool, error) {
|
||||
cfgFile, err := os.CreateTemp("", "matugen-config-*.toml")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp config: %w", err)
|
||||
return false, fmt.Errorf("failed to create temp config: %w", err)
|
||||
}
|
||||
defer os.Remove(cfgFile.Name())
|
||||
defer cfgFile.Close()
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "matugen-templates-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp dir: %w", err)
|
||||
return false, fmt.Errorf("failed to create temp dir: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
if err := buildMergedConfig(opts, cfgFile, tmpDir); err != nil {
|
||||
return fmt.Errorf("failed to build config: %w", err)
|
||||
return false, fmt.Errorf("failed to build config: %w", err)
|
||||
}
|
||||
cfgFile.Close()
|
||||
|
||||
oldColors, _ := os.ReadFile(opts.ColorsOutput())
|
||||
|
||||
var primaryDark, primaryLight, surface string
|
||||
var dank16JSON string
|
||||
var importArgs []string
|
||||
@@ -200,7 +214,7 @@ func buildOnce(opts *Options) error {
|
||||
surface = extractNestedColor(opts.StockColors, "surface", "dark")
|
||||
|
||||
if primaryDark == "" {
|
||||
return fmt.Errorf("failed to extract primary dark from stock colors")
|
||||
return false, fmt.Errorf("failed to extract primary dark from stock colors")
|
||||
}
|
||||
if primaryLight == "" {
|
||||
primaryLight = primaryDark
|
||||
@@ -214,14 +228,14 @@ func buildOnce(opts *Options) error {
|
||||
args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()}
|
||||
args = append(args, importArgs...)
|
||||
if err := runMatugen(args); err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
} else {
|
||||
log.Infof("Using dynamic theme from %s: %s", opts.Kind, opts.Value)
|
||||
|
||||
matJSON, err := runMatugenDryRun(opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("matugen dry-run failed: %w", err)
|
||||
return false, fmt.Errorf("matugen dry-run failed: %w", err)
|
||||
}
|
||||
|
||||
primaryDark = extractMatugenColor(matJSON, "primary", "dark")
|
||||
@@ -229,7 +243,7 @@ func buildOnce(opts *Options) error {
|
||||
surface = extractMatugenColor(matJSON, "surface", "dark")
|
||||
|
||||
if primaryDark == "" {
|
||||
return fmt.Errorf("failed to extract primary color")
|
||||
return false, fmt.Errorf("failed to extract primary color")
|
||||
}
|
||||
if primaryLight == "" {
|
||||
primaryLight = primaryDark
|
||||
@@ -250,10 +264,15 @@ func buildOnce(opts *Options) error {
|
||||
args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name())
|
||||
args = append(args, importArgs...)
|
||||
if err := runMatugen(args); err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
newColors, _ := os.ReadFile(opts.ColorsOutput())
|
||||
if bytes.Equal(oldColors, newColors) && len(oldColors) > 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if isDMSGTKActive(opts.ConfigDir) {
|
||||
switch opts.Mode {
|
||||
case ColorModeLight:
|
||||
@@ -271,7 +290,7 @@ func buildOnce(opts *Options) error {
|
||||
|
||||
signalTerminals(opts)
|
||||
|
||||
return nil
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
|
||||
@@ -334,6 +353,10 @@ output_path = '%s'
|
||||
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir)
|
||||
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions"), opts.ShellDir)
|
||||
appendVSCodeConfig(cfgFile, "vscode-insiders", filepath.Join(homeDir, ".vscode-insiders/extensions"), opts.ShellDir)
|
||||
case TemplateKindEmacs:
|
||||
if utils.EmacsConfigDir() != "" {
|
||||
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
|
||||
}
|
||||
default:
|
||||
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
|
||||
}
|
||||
@@ -491,6 +514,9 @@ func substituteVars(content, shellDir string) string {
|
||||
result = strings.ReplaceAll(result, "'CONFIG_DIR/", "'"+utils.XDGConfigHome()+"/")
|
||||
result = strings.ReplaceAll(result, "'DATA_DIR/", "'"+utils.XDGDataHome()+"/")
|
||||
result = strings.ReplaceAll(result, "'CACHE_DIR/", "'"+utils.XDGCacheHome()+"/")
|
||||
if emacsDir := utils.EmacsConfigDir(); emacsDir != "" {
|
||||
result = strings.ReplaceAll(result, "'EMACS_DIR/", "'"+emacsDir+"/")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -511,79 +537,160 @@ func extractTOMLSection(content, startMarker, endMarker string) string {
|
||||
return content[startIdx : startIdx+endIdx]
|
||||
}
|
||||
|
||||
func checkMatugenVersion() {
|
||||
matugenVersionOnce.Do(func() {
|
||||
cmd := exec.Command("matugen", "--version")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
versionStr := strings.TrimSpace(string(output))
|
||||
versionStr = strings.TrimPrefix(versionStr, "matugen ")
|
||||
|
||||
parts := strings.Split(versionStr, ".")
|
||||
if len(parts) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
major, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
minor, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
matugenSupportsCOE = major > 3 || (major == 3 && minor >= 1)
|
||||
matugenIsV4 = major >= 4
|
||||
if matugenSupportsCOE {
|
||||
log.Infof("Matugen %s supports --continue-on-error", versionStr)
|
||||
}
|
||||
if matugenIsV4 {
|
||||
log.Infof("Matugen %s: using v4 flags", versionStr)
|
||||
}
|
||||
})
|
||||
type matugenFlags struct {
|
||||
supportsCOE bool
|
||||
isV4 bool
|
||||
}
|
||||
|
||||
func runMatugen(args []string) error {
|
||||
checkMatugenVersion()
|
||||
func detectMatugenVersion() (matugenFlags, error) {
|
||||
matugenVersionMu.Lock()
|
||||
defer matugenVersionMu.Unlock()
|
||||
|
||||
if matugenVersionOK {
|
||||
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
|
||||
}
|
||||
|
||||
return detectMatugenVersionLocked()
|
||||
}
|
||||
|
||||
func redetectMatugenVersion(old matugenFlags) (matugenFlags, bool) {
|
||||
matugenVersionMu.Lock()
|
||||
defer matugenVersionMu.Unlock()
|
||||
|
||||
matugenVersionOK = false
|
||||
flags, err := detectMatugenVersionLocked()
|
||||
if err != nil {
|
||||
return old, false
|
||||
}
|
||||
changed := flags.supportsCOE != old.supportsCOE || flags.isV4 != old.isV4
|
||||
return flags, changed
|
||||
}
|
||||
|
||||
func detectMatugenVersionLocked() (matugenFlags, error) {
|
||||
cmd := exec.Command("matugen", "--version")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return matugenFlags{}, fmt.Errorf("failed to get matugen version: %w", err)
|
||||
}
|
||||
|
||||
versionStr := strings.TrimSpace(string(output))
|
||||
versionStr = strings.TrimPrefix(versionStr, "matugen ")
|
||||
|
||||
parts := strings.Split(versionStr, ".")
|
||||
if len(parts) < 2 {
|
||||
return matugenFlags{}, fmt.Errorf("unexpected matugen version format: %q", versionStr)
|
||||
}
|
||||
|
||||
major, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return matugenFlags{}, fmt.Errorf("failed to parse matugen major version %q: %w", parts[0], err)
|
||||
}
|
||||
|
||||
minor, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return matugenFlags{}, fmt.Errorf("failed to parse matugen minor version %q: %w", parts[1], err)
|
||||
}
|
||||
|
||||
matugenSupportsCOE = major > 3 || (major == 3 && minor >= 1)
|
||||
matugenIsV4 = major >= 4
|
||||
matugenVersionOK = true
|
||||
|
||||
if matugenSupportsCOE {
|
||||
args = append([]string{"--continue-on-error"}, args...)
|
||||
log.Infof("Matugen %s supports --continue-on-error", versionStr)
|
||||
}
|
||||
if matugenIsV4 {
|
||||
log.Infof("Matugen %s: using v4 flags", versionStr)
|
||||
}
|
||||
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
|
||||
}
|
||||
|
||||
func buildMatugenArgs(baseArgs []string, flags matugenFlags) []string {
|
||||
args := make([]string, 0, len(baseArgs)+4)
|
||||
if flags.supportsCOE {
|
||||
args = append(args, "--continue-on-error")
|
||||
}
|
||||
args = append(args, baseArgs...)
|
||||
if flags.isV4 {
|
||||
args = append(args, "--source-color-index", "0")
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func runMatugen(baseArgs []string) error {
|
||||
flags, err := detectMatugenVersion()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args := buildMatugenArgs(baseArgs, flags)
|
||||
cmd := exec.Command("matugen", args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
runErr := cmd.Run()
|
||||
if runErr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Warnf("Matugen failed (v4=%v): %v", flags.isV4, runErr)
|
||||
|
||||
newFlags, changed := redetectMatugenVersion(flags)
|
||||
if !changed {
|
||||
return runErr
|
||||
}
|
||||
|
||||
log.Warnf("Matugen version changed (v4: %v -> %v), retrying", flags.isV4, newFlags.isV4)
|
||||
args = buildMatugenArgs(baseArgs, newFlags)
|
||||
retryCmd := exec.Command("matugen", args...)
|
||||
retryCmd.Stdout = os.Stdout
|
||||
retryCmd.Stderr = os.Stderr
|
||||
return retryCmd.Run()
|
||||
}
|
||||
|
||||
func runMatugenDryRun(opts *Options) (string, error) {
|
||||
checkMatugenVersion()
|
||||
|
||||
var args []string
|
||||
switch opts.Kind {
|
||||
case "hex":
|
||||
args = []string{"color", "hex", opts.Value}
|
||||
default:
|
||||
args = []string{opts.Kind, opts.Value}
|
||||
}
|
||||
args = append(args, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
|
||||
if matugenIsV4 {
|
||||
args = append(args, "--source-color-index", "0", "--old-json-output")
|
||||
}
|
||||
|
||||
cmd := exec.Command("matugen", args...)
|
||||
output, err := cmd.Output()
|
||||
flags, err := detectMatugenVersion()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
output, dryErr := execDryRun(opts, flags)
|
||||
if dryErr == nil {
|
||||
return output, nil
|
||||
}
|
||||
|
||||
log.Warnf("Matugen dry-run failed (v4=%v): %v", flags.isV4, dryErr)
|
||||
|
||||
newFlags, changed := redetectMatugenVersion(flags)
|
||||
if !changed {
|
||||
return "", dryErr
|
||||
}
|
||||
|
||||
log.Warnf("Matugen version changed (v4: %v -> %v), retrying dry-run", flags.isV4, newFlags.isV4)
|
||||
return execDryRun(opts, newFlags)
|
||||
}
|
||||
|
||||
func execDryRun(opts *Options, flags matugenFlags) (string, error) {
|
||||
var baseArgs []string
|
||||
switch opts.Kind {
|
||||
case "hex":
|
||||
baseArgs = []string{"color", "hex", opts.Value}
|
||||
default:
|
||||
baseArgs = []string{opts.Kind, opts.Value}
|
||||
}
|
||||
baseArgs = append(baseArgs, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
|
||||
if flags.isV4 {
|
||||
baseArgs = append(baseArgs, "--source-color-index", "0", "--old-json-output")
|
||||
}
|
||||
|
||||
cmd := exec.Command("matugen", baseArgs...)
|
||||
var stderr strings.Builder
|
||||
cmd.Stderr = &stderr
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
if stderr.Len() > 0 {
|
||||
return "", fmt.Errorf("matugen %v failed (v4=%v): %s", baseArgs, flags.isV4, strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
return "", fmt.Errorf("matugen %v failed (v4=%v): %w", baseArgs, flags.isV4, err)
|
||||
}
|
||||
return strings.ReplaceAll(string(output), "\n", ""), nil
|
||||
}
|
||||
|
||||
@@ -819,6 +926,8 @@ func CheckTemplates(checker utils.AppChecker) []TemplateCheck {
|
||||
detected = true
|
||||
case tmpl.Kind == TemplateKindVSCode:
|
||||
detected = checkVSCodeExtension(homeDir)
|
||||
case tmpl.Kind == TemplateKindEmacs:
|
||||
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks) && utils.EmacsConfigDir() != ""
|
||||
default:
|
||||
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package matugen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
@@ -93,10 +94,13 @@ func (q *Queue) runWorker() {
|
||||
err := Run(job.Options)
|
||||
|
||||
var result Result
|
||||
if err != nil {
|
||||
result = Result{Success: false, Error: err}
|
||||
} else {
|
||||
switch {
|
||||
case err == nil:
|
||||
result = Result{Success: true}
|
||||
case errors.Is(err, ErrNoChanges):
|
||||
result = Result{Success: true}
|
||||
default:
|
||||
result = Result{Success: false, Error: err}
|
||||
}
|
||||
|
||||
q.finishJob(result)
|
||||
|
||||
@@ -258,7 +258,7 @@ func (i *ExtWorkspaceManagerV1) Dispatch(opcode uint32, fd int, data []byte) {
|
||||
l := 0
|
||||
objectID := client.Uint32(data[l : l+4])
|
||||
proxy := i.Context().GetProxy(objectID)
|
||||
if proxy != nil {
|
||||
if proxy != nil && !proxy.IsZombie() {
|
||||
e.WorkspaceGroup = proxy.(*ExtWorkspaceGroupHandleV1)
|
||||
} else {
|
||||
groupHandle := &ExtWorkspaceGroupHandleV1{}
|
||||
@@ -278,7 +278,7 @@ func (i *ExtWorkspaceManagerV1) Dispatch(opcode uint32, fd int, data []byte) {
|
||||
l := 0
|
||||
objectID := client.Uint32(data[l : l+4])
|
||||
proxy := i.Context().GetProxy(objectID)
|
||||
if proxy != nil {
|
||||
if proxy != nil && !proxy.IsZombie() {
|
||||
e.Workspace = proxy.(*ExtWorkspaceHandleV1)
|
||||
} else {
|
||||
wsHandle := &ExtWorkspaceHandleV1{}
|
||||
|
||||
@@ -108,7 +108,7 @@ func NewRegionSelector(s *Screenshoter) *RegionSelector {
|
||||
screenshoter: s,
|
||||
outputs: make(map[uint32]*WaylandOutput),
|
||||
preCapture: make(map[*WaylandOutput]*PreCapture),
|
||||
showCapturedCursor: true,
|
||||
showCapturedCursor: s.config.Cursor == CursorOn,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -453,10 +453,7 @@ func (s *Screenshoter) blitBuffer(dst, src *ShmBuffer, dstX, dstY int, yInverted
|
||||
}
|
||||
|
||||
func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult, error) {
|
||||
cursor := int32(0)
|
||||
if s.config.IncludeCursor {
|
||||
cursor = 1
|
||||
}
|
||||
cursor := int32(s.config.Cursor)
|
||||
|
||||
frame, err := s.screencopy.CaptureOutput(cursor, output.wlOutput)
|
||||
if err != nil {
|
||||
@@ -624,10 +621,7 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
|
||||
}
|
||||
}
|
||||
|
||||
cursor := int32(0)
|
||||
if s.config.IncludeCursor {
|
||||
cursor = 1
|
||||
}
|
||||
cursor := int32(s.config.Cursor)
|
||||
|
||||
frame, err := s.screencopy.CaptureOutputRegion(cursor, output.wlOutput, localX, localY, w, h)
|
||||
if err != nil {
|
||||
|
||||
@@ -19,6 +19,13 @@ const (
|
||||
FormatPPM
|
||||
)
|
||||
|
||||
type CursorMode int
|
||||
|
||||
const (
|
||||
CursorOff CursorMode = iota
|
||||
CursorOn
|
||||
)
|
||||
|
||||
type Region struct {
|
||||
X int32 `json:"x"`
|
||||
Y int32 `json:"y"`
|
||||
@@ -42,29 +49,29 @@ type Output struct {
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Mode Mode
|
||||
OutputName string
|
||||
IncludeCursor bool
|
||||
Format Format
|
||||
Quality int
|
||||
OutputDir string
|
||||
Filename string
|
||||
Clipboard bool
|
||||
SaveFile bool
|
||||
Notify bool
|
||||
Stdout bool
|
||||
Mode Mode
|
||||
OutputName string
|
||||
Cursor CursorMode
|
||||
Format Format
|
||||
Quality int
|
||||
OutputDir string
|
||||
Filename string
|
||||
Clipboard bool
|
||||
SaveFile bool
|
||||
Notify bool
|
||||
Stdout bool
|
||||
}
|
||||
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
Mode: ModeRegion,
|
||||
IncludeCursor: false,
|
||||
Format: FormatPNG,
|
||||
Quality: 90,
|
||||
OutputDir: "",
|
||||
Filename: "",
|
||||
Clipboard: true,
|
||||
SaveFile: true,
|
||||
Notify: true,
|
||||
Mode: ModeRegion,
|
||||
Cursor: CursorOff,
|
||||
Format: FormatPNG,
|
||||
Quality: 90,
|
||||
OutputDir: "",
|
||||
Filename: "",
|
||||
Clipboard: true,
|
||||
SaveFile: true,
|
||||
Notify: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,6 +311,10 @@ func (m *Manager) handleDevicePropertiesChanged(path dbus.ObjectPath, changed ma
|
||||
select {
|
||||
case m.eventQueue <- func() {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
log.Infof("[Bluetooth] Auto-trusting newly paired device: %s", devicePath)
|
||||
if err := m.TrustDevice(devicePath, true); err != nil {
|
||||
log.Warnf("[Bluetooth] Auto-trust failed: %v", err)
|
||||
}
|
||||
log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath)
|
||||
if err := m.ConnectDevice(devicePath); err != nil {
|
||||
log.Warnf("[Bluetooth] Auto-connect failed: %v", err)
|
||||
|
||||
@@ -232,8 +232,15 @@ func (m *Manager) setupDataDeviceSync() {
|
||||
return
|
||||
}
|
||||
|
||||
prevOffer := m.currentOffer
|
||||
m.currentOffer = offer
|
||||
|
||||
if prevOffer != nil && prevOffer != offer {
|
||||
m.offerMutex.Lock()
|
||||
delete(m.offerMimeTypes, prevOffer)
|
||||
m.offerMutex.Unlock()
|
||||
}
|
||||
|
||||
m.offerMutex.RLock()
|
||||
mimes := m.offerMimeTypes[offer]
|
||||
m.offerMutex.RUnlock()
|
||||
@@ -587,20 +594,26 @@ func (m *Manager) uriListPreview(data []byte) (string, bool) {
|
||||
uris = strings.Split(text, "\n")
|
||||
}
|
||||
|
||||
if len(uris) > 1 {
|
||||
return fmt.Sprintf("[[ %d files ]]", len(uris)), false
|
||||
}
|
||||
|
||||
if len(uris) == 1 && strings.HasPrefix(uris[0], "file://") {
|
||||
filePath := strings.TrimPrefix(uris[0], "file://")
|
||||
if info, err := os.Stat(filePath); err == nil && !info.IsDir() {
|
||||
info, err := os.Stat(filePath)
|
||||
if err != nil || info.IsDir() {
|
||||
return m.textPreview(data), false
|
||||
}
|
||||
|
||||
cfg := m.getConfig()
|
||||
if info.Size() <= cfg.MaxEntrySize {
|
||||
if imgData, err := os.ReadFile(filePath); err == nil {
|
||||
if config, imgFmt, err := image.DecodeConfig(bytes.NewReader(imgData)); err == nil {
|
||||
return fmt.Sprintf("[[ file %s %s %dx%d ]]", filepath.Base(filePath), imgFmt, config.Width, config.Height), true
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("[[ file %s ]]", filepath.Base(filePath)), false
|
||||
}
|
||||
}
|
||||
|
||||
if len(uris) > 1 {
|
||||
return fmt.Sprintf("[[ %d files ]]", len(uris)), false
|
||||
return fmt.Sprintf("[[ file %s ]]", filepath.Base(filePath)), false
|
||||
}
|
||||
|
||||
return m.textPreview(data), false
|
||||
@@ -623,6 +636,11 @@ func (m *Manager) tryReadImageFromURI(data []byte) ([]byte, string, bool) {
|
||||
return nil, "", false
|
||||
}
|
||||
|
||||
cfg := m.getConfig()
|
||||
if info.Size() > cfg.MaxEntrySize {
|
||||
return nil, "", false
|
||||
}
|
||||
|
||||
imgData, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, "", false
|
||||
|
||||
@@ -16,4 +16,8 @@ const (
|
||||
dbusScreensaverPath = "/ScreenSaver"
|
||||
dbusScreensaverPath2 = "/org/freedesktop/ScreenSaver"
|
||||
dbusScreensaverInterface = "org.freedesktop.ScreenSaver"
|
||||
|
||||
dbusGnomeScreensaverName = "org.gnome.ScreenSaver"
|
||||
dbusGnomeScreensaverPath = "/org/gnome/ScreenSaver"
|
||||
dbusGnomeScreensaverInterface = "org.gnome.ScreenSaver"
|
||||
)
|
||||
|
||||
@@ -191,6 +191,12 @@ func (m *Manager) Close() {
|
||||
return true
|
||||
})
|
||||
|
||||
m.screensaverSubscribers.Range(func(key string, ch chan ScreensaverState) bool {
|
||||
close(ch)
|
||||
m.screensaverSubscribers.Delete(key)
|
||||
return true
|
||||
})
|
||||
|
||||
if m.systemConn != nil {
|
||||
m.systemConn.Close()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package freedesktop
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
@@ -15,45 +16,9 @@ type screensaverHandler struct {
|
||||
manager *Manager
|
||||
}
|
||||
|
||||
func (m *Manager) initializeScreensaver() error {
|
||||
if m.sessionConn == nil {
|
||||
m.stateMutex.Lock()
|
||||
m.state.Screensaver.Available = false
|
||||
m.stateMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
reply, err := m.sessionConn.RequestName(dbusScreensaverName, dbus.NameFlagDoNotQueue)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to request screensaver name: %v", err)
|
||||
m.stateMutex.Lock()
|
||||
m.state.Screensaver.Available = false
|
||||
m.stateMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
if reply != dbus.RequestNameReplyPrimaryOwner {
|
||||
log.Warnf("Screensaver name already owned by another process")
|
||||
m.stateMutex.Lock()
|
||||
m.state.Screensaver.Available = false
|
||||
m.stateMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
handler := &screensaverHandler{manager: m}
|
||||
|
||||
if err := m.sessionConn.Export(handler, dbusScreensaverPath, dbusScreensaverInterface); err != nil {
|
||||
log.Warnf("Failed to export screensaver on %s: %v", dbusScreensaverPath, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := m.sessionConn.Export(handler, dbusScreensaverPath2, dbusScreensaverInterface); err != nil {
|
||||
log.Warnf("Failed to export screensaver on %s: %v", dbusScreensaverPath2, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
screensaverIface := introspect.Interface{
|
||||
Name: dbusScreensaverInterface,
|
||||
func screensaverIntrospectIface(ifaceName string) introspect.Interface {
|
||||
return introspect.Interface{
|
||||
Name: ifaceName,
|
||||
Methods: []introspect.Method{
|
||||
{
|
||||
Name: "Inhibit",
|
||||
@@ -69,40 +34,106 @@ func (m *Manager) initializeScreensaver() error {
|
||||
{Name: "cookie", Type: "u", Direction: "in"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "GetActive",
|
||||
Args: []introspect.Arg{
|
||||
{Name: "active", Type: "b", Direction: "out"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "SetActive",
|
||||
Args: []introspect.Arg{
|
||||
{Name: "active", Type: "b", Direction: "in"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Lock",
|
||||
},
|
||||
},
|
||||
Signals: []introspect.Signal{
|
||||
{
|
||||
Name: "ActiveChanged",
|
||||
Args: []introspect.Arg{
|
||||
{Name: "new_value", Type: "b"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) initializeScreensaver() error {
|
||||
if m.sessionConn == nil {
|
||||
m.stateMutex.Lock()
|
||||
m.state.Screensaver.Available = false
|
||||
m.stateMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
introNode := &introspect.Node{
|
||||
Name: dbusScreensaverPath,
|
||||
Interfaces: []introspect.Interface{
|
||||
introspect.IntrospectData,
|
||||
screensaverIface,
|
||||
},
|
||||
}
|
||||
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode), dbusScreensaverPath, "org.freedesktop.DBus.Introspectable"); err != nil {
|
||||
log.Warnf("Failed to export introspectable on %s: %v", dbusScreensaverPath, err)
|
||||
}
|
||||
handler := &screensaverHandler{manager: m}
|
||||
|
||||
introNode2 := &introspect.Node{
|
||||
Name: dbusScreensaverPath2,
|
||||
Interfaces: []introspect.Interface{
|
||||
introspect.IntrospectData,
|
||||
screensaverIface,
|
||||
},
|
||||
}
|
||||
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode2), dbusScreensaverPath2, "org.freedesktop.DBus.Introspectable"); err != nil {
|
||||
log.Warnf("Failed to export introspectable on %s: %v", dbusScreensaverPath2, err)
|
||||
m.screensaverFreedesktopClaimed = m.claimScreensaverName(handler,
|
||||
dbusScreensaverName, dbusScreensaverInterface, dbusScreensaverPath, dbusScreensaverPath2)
|
||||
m.screensaverGnomeClaimed = m.claimScreensaverName(handler,
|
||||
dbusGnomeScreensaverName, dbusGnomeScreensaverInterface, dbusGnomeScreensaverPath)
|
||||
|
||||
if !m.screensaverFreedesktopClaimed && !m.screensaverGnomeClaimed {
|
||||
log.Warn("No screensaver interface could be claimed")
|
||||
m.stateMutex.Lock()
|
||||
m.state.Screensaver.Available = false
|
||||
m.stateMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
go m.watchPeerDisconnects()
|
||||
|
||||
m.stateMutex.Lock()
|
||||
m.state.Screensaver.Available = true
|
||||
m.state.Screensaver.Active = false
|
||||
m.state.Screensaver.Inhibited = false
|
||||
m.state.Screensaver.Inhibitors = []ScreensaverInhibitor{}
|
||||
m.stateMutex.Unlock()
|
||||
|
||||
log.Info("Screensaver inhibit listener initialized")
|
||||
log.Info("Screensaver listener initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) claimScreensaverName(handler *screensaverHandler, name, iface string, paths ...dbus.ObjectPath) bool {
|
||||
reply, err := m.sessionConn.RequestName(name, dbus.NameFlagDoNotQueue)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to request screensaver name %s: %v", name, err)
|
||||
return false
|
||||
}
|
||||
if reply != dbus.RequestNameReplyPrimaryOwner {
|
||||
log.Warnf("Screensaver name %s already owned by another process", name)
|
||||
return false
|
||||
}
|
||||
if err := m.exportScreensaverOnPaths(handler, iface, paths...); err != nil {
|
||||
log.Warnf("Failed to export screensaver on %s: %v", name, err)
|
||||
return false
|
||||
}
|
||||
log.Infof("Claimed %s on session bus", name)
|
||||
return true
|
||||
}
|
||||
|
||||
// exportScreensaverOnPaths exports the handler and introspection on the given
|
||||
// paths under the specified interface name.
|
||||
func (m *Manager) exportScreensaverOnPaths(handler *screensaverHandler, ifaceName string, paths ...dbus.ObjectPath) error {
|
||||
iface := screensaverIntrospectIface(ifaceName)
|
||||
for _, path := range paths {
|
||||
if err := m.sessionConn.Export(handler, path, ifaceName); err != nil {
|
||||
return fmt.Errorf("export handler on %s: %w", path, err)
|
||||
}
|
||||
node := &introspect.Node{
|
||||
Name: string(path),
|
||||
Interfaces: []introspect.Interface{
|
||||
introspect.IntrospectData,
|
||||
iface,
|
||||
},
|
||||
}
|
||||
if err := m.sessionConn.Export(introspect.NewIntrospectable(node), path, "org.freedesktop.DBus.Introspectable"); err != nil {
|
||||
log.Warnf("Failed to export introspectable on %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -268,3 +299,51 @@ func (m *Manager) NotifyScreensaverSubscribers() {
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (h *screensaverHandler) GetActive() (bool, *dbus.Error) {
|
||||
h.manager.stateMutex.RLock()
|
||||
active := h.manager.state.Screensaver.Active
|
||||
h.manager.stateMutex.RUnlock()
|
||||
return active, nil
|
||||
}
|
||||
|
||||
func (h *screensaverHandler) SetActive(active bool) *dbus.Error {
|
||||
h.manager.SetScreenLockActive(active)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *screensaverHandler) Lock() *dbus.Error {
|
||||
h.manager.SetScreenLockActive(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) SetScreenLockActive(active bool) {
|
||||
m.stateMutex.Lock()
|
||||
changed := m.state.Screensaver.Active != active
|
||||
m.state.Screensaver.Active = active
|
||||
m.stateMutex.Unlock()
|
||||
|
||||
if !changed {
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("Screen lock active changed: %v", active)
|
||||
defer m.NotifyScreensaverSubscribers()
|
||||
|
||||
if m.sessionConn == nil {
|
||||
return
|
||||
}
|
||||
if m.screensaverFreedesktopClaimed {
|
||||
if err := m.sessionConn.Emit(dbusScreensaverPath, dbusScreensaverInterface+".ActiveChanged", active); err != nil {
|
||||
log.Warnf("Failed to emit ActiveChanged on %s: %v", dbusScreensaverPath, err)
|
||||
}
|
||||
if err := m.sessionConn.Emit(dbusScreensaverPath2, dbusScreensaverInterface+".ActiveChanged", active); err != nil {
|
||||
log.Warnf("Failed to emit ActiveChanged on %s: %v", dbusScreensaverPath2, err)
|
||||
}
|
||||
}
|
||||
if m.screensaverGnomeClaimed {
|
||||
if err := m.sessionConn.Emit(dbusGnomeScreensaverPath, dbusGnomeScreensaverInterface+".ActiveChanged", active); err != nil {
|
||||
log.Warnf("Failed to emit ActiveChanged on %s: %v", dbusGnomeScreensaverPath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
102
core/internal/server/freedesktop/screensaver_test.go
Normal file
102
core/internal/server/freedesktop/screensaver_test.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package freedesktop
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSetScreenLockActive_ChangesState(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Screensaver: ScreensaverState{Available: true},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
assert.False(t, manager.GetScreensaverState().Active)
|
||||
|
||||
manager.SetScreenLockActive(true)
|
||||
assert.True(t, manager.GetScreensaverState().Active)
|
||||
|
||||
manager.SetScreenLockActive(false)
|
||||
assert.False(t, manager.GetScreensaverState().Active)
|
||||
}
|
||||
|
||||
func TestSetScreenLockActive_NoChangeNoDuplicate(t *testing.T) {
|
||||
ch := make(chan ScreensaverState, 64)
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Screensaver: ScreensaverState{Available: true, Active: false},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
manager.screensaverSubscribers.Store("test", ch)
|
||||
defer manager.screensaverSubscribers.Delete("test")
|
||||
|
||||
// Setting to same value should not notify
|
||||
manager.SetScreenLockActive(false)
|
||||
|
||||
select {
|
||||
case <-ch:
|
||||
t.Fatal("should not have received notification for no-change")
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
// Expected: no notification
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetScreenLockActive_NotifiesSubscribers(t *testing.T) {
|
||||
ch := make(chan ScreensaverState, 64)
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Screensaver: ScreensaverState{Available: true, Active: false},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
manager.screensaverSubscribers.Store("test", ch)
|
||||
defer manager.screensaverSubscribers.Delete("test")
|
||||
|
||||
manager.SetScreenLockActive(true)
|
||||
|
||||
select {
|
||||
case state := <-ch:
|
||||
assert.True(t, state.Active)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timeout waiting for subscriber notification")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetScreenLockActive_NilSessionConn(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Screensaver: ScreensaverState{Available: true},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
manager.SetScreenLockActive(true)
|
||||
})
|
||||
assert.True(t, manager.GetScreensaverState().Active)
|
||||
}
|
||||
|
||||
func TestGetActive_ReturnsCurrentState(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Screensaver: ScreensaverState{Available: true, Active: true},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
handler := &screensaverHandler{manager: manager}
|
||||
active, dbusErr := handler.GetActive()
|
||||
assert.Nil(t, dbusErr)
|
||||
assert.True(t, active)
|
||||
}
|
||||
|
||||
func TestScreensaverState_ActiveDefaultsFalse(t *testing.T) {
|
||||
state := ScreensaverState{}
|
||||
assert.False(t, state.Active)
|
||||
}
|
||||
@@ -39,6 +39,7 @@ type ScreensaverInhibitor struct {
|
||||
|
||||
type ScreensaverState struct {
|
||||
Available bool `json:"available"`
|
||||
Active bool `json:"active"`
|
||||
Inhibited bool `json:"inhibited"`
|
||||
Inhibitors []ScreensaverInhibitor `json:"inhibitors"`
|
||||
}
|
||||
@@ -50,14 +51,16 @@ type FreedeskState struct {
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
state *FreedeskState
|
||||
stateMutex sync.RWMutex
|
||||
systemConn *dbus.Conn
|
||||
sessionConn *dbus.Conn
|
||||
accountsObj dbus.BusObject
|
||||
settingsObj dbus.BusObject
|
||||
currentUID uint64
|
||||
subscribers syncmap.Map[string, chan FreedeskState]
|
||||
screensaverSubscribers syncmap.Map[string, chan ScreensaverState]
|
||||
screensaverCookieCounter uint32
|
||||
state *FreedeskState
|
||||
stateMutex sync.RWMutex
|
||||
systemConn *dbus.Conn
|
||||
sessionConn *dbus.Conn
|
||||
accountsObj dbus.BusObject
|
||||
settingsObj dbus.BusObject
|
||||
currentUID uint64
|
||||
subscribers syncmap.Map[string, chan FreedeskState]
|
||||
screensaverSubscribers syncmap.Map[string, chan ScreensaverState]
|
||||
screensaverCookieCounter uint32
|
||||
screensaverFreedesktopClaimed bool
|
||||
screensaverGnomeClaimed bool
|
||||
}
|
||||
|
||||
@@ -1516,7 +1516,11 @@ func Start(printDocs bool) error {
|
||||
}
|
||||
}()
|
||||
|
||||
loginctlReady := make(chan struct{})
|
||||
freedesktopReady := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(loginctlReady)
|
||||
if err := InitializeLoginctlManager(); err != nil {
|
||||
log.Warnf("Loginctl manager unavailable: %v", err)
|
||||
} else {
|
||||
@@ -1525,6 +1529,7 @@ func Start(printDocs bool) error {
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer close(freedesktopReady)
|
||||
if err := InitializeFreedeskManager(); err != nil {
|
||||
log.Warnf("Freedesktop manager unavailable: %v", err)
|
||||
} else if freedesktopManager != nil {
|
||||
@@ -1533,6 +1538,31 @@ func Start(printDocs bool) error {
|
||||
}
|
||||
}()
|
||||
|
||||
// Bridge loginctl lock state to the freedesktop/gnome screensaver
|
||||
// ActiveChanged signal so apps like Bitwarden can detect screen lock.
|
||||
go func() {
|
||||
<-loginctlReady
|
||||
<-freedesktopReady
|
||||
|
||||
if loginctlManager == nil || freedesktopManager == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ch := loginctlManager.Subscribe("dms-lock-bridge")
|
||||
defer loginctlManager.Unsubscribe("dms-lock-bridge")
|
||||
|
||||
initial := loginctlManager.GetState()
|
||||
lastLocked := initial.Locked
|
||||
freedesktopManager.SetScreenLockActive(lastLocked)
|
||||
|
||||
for state := range ch {
|
||||
if state.Locked != lastLocked {
|
||||
lastLocked = state.Locked
|
||||
freedesktopManager.SetScreenLockActive(lastLocked)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err := InitializeWaylandManager(); err != nil {
|
||||
log.Warnf("Wayland manager unavailable: %v", err)
|
||||
}
|
||||
@@ -1569,6 +1599,13 @@ func Start(printDocs bool) error {
|
||||
log.Warnf("Theme mode manager unavailable: %v", err)
|
||||
} else {
|
||||
notifyCapabilityChange()
|
||||
go func() {
|
||||
<-loginctlReady
|
||||
if loginctlManager == nil {
|
||||
return
|
||||
}
|
||||
themeModeManager.WatchLoginctl(loginctlManager)
|
||||
}()
|
||||
}
|
||||
|
||||
fatalErrChan := make(chan error, 1)
|
||||
|
||||
@@ -162,7 +162,7 @@ func TestCleanupStaleSockets(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
t.Setenv("XDG_RUNTIME_DIR", tempDir)
|
||||
|
||||
staleSocket := filepath.Join(tempDir, "danklinux-999999.sock")
|
||||
staleSocket := filepath.Join(tempDir, "danklinux-4194305.sock")
|
||||
err := os.WriteFile(staleSocket, []byte{}, 0o600)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
)
|
||||
@@ -187,6 +188,29 @@ func (m *Manager) Close() {
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) WatchLoginctl(lm *loginctl.Manager) {
|
||||
ch := lm.Subscribe("thememode")
|
||||
m.wg.Add(1)
|
||||
go func() {
|
||||
defer m.wg.Done()
|
||||
defer lm.Unsubscribe("thememode")
|
||||
for {
|
||||
select {
|
||||
case <-m.stopChan:
|
||||
return
|
||||
case state, ok := <-ch:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if state.PreparingForSleep {
|
||||
continue
|
||||
}
|
||||
m.TriggerUpdate()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (m *Manager) schedulerLoop() {
|
||||
defer m.wg.Done()
|
||||
|
||||
@@ -327,10 +351,12 @@ func statesEqual(a, b *State) bool {
|
||||
}
|
||||
|
||||
func (m *Manager) computeSchedule(now time.Time, config Config) (bool, time.Time) {
|
||||
if config.Mode == "location" {
|
||||
switch config.Mode {
|
||||
case "location":
|
||||
return m.computeLocationSchedule(now, config)
|
||||
default:
|
||||
return computeTimeSchedule(now, config)
|
||||
}
|
||||
return computeTimeSchedule(now, config)
|
||||
}
|
||||
|
||||
func computeTimeSchedule(now time.Time, config Config) (bool, time.Time) {
|
||||
@@ -381,10 +407,10 @@ func (m *Manager) computeLocationSchedule(now time.Time, config Config) (bool, t
|
||||
}
|
||||
|
||||
times, cond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, now, config.ElevationTwilight, config.ElevationDaylight)
|
||||
if cond != wayland.SunNormal {
|
||||
if cond == wayland.SunMidnightSun {
|
||||
return true, startOfNextDay(now)
|
||||
}
|
||||
switch cond {
|
||||
case wayland.SunMidnightSun:
|
||||
return true, startOfNextDay(now)
|
||||
case wayland.SunPolarNight:
|
||||
return false, startOfNextDay(now)
|
||||
}
|
||||
|
||||
@@ -397,10 +423,10 @@ func (m *Manager) computeLocationSchedule(now time.Time, config Config) (bool, t
|
||||
|
||||
nextDay := startOfNextDay(now)
|
||||
nextTimes, nextCond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, nextDay, config.ElevationTwilight, config.ElevationDaylight)
|
||||
if nextCond != wayland.SunNormal {
|
||||
if nextCond == wayland.SunMidnightSun {
|
||||
return true, startOfNextDay(nextDay)
|
||||
}
|
||||
switch nextCond {
|
||||
case wayland.SunMidnightSun:
|
||||
return true, startOfNextDay(nextDay)
|
||||
case wayland.SunPolarNight:
|
||||
return false, startOfNextDay(nextDay)
|
||||
}
|
||||
|
||||
@@ -413,13 +439,7 @@ func startOfNextDay(t time.Time) time.Time {
|
||||
}
|
||||
|
||||
func validateHourMinute(hour, minute int) bool {
|
||||
if hour < 0 || hour > 23 {
|
||||
return false
|
||||
}
|
||||
if minute < 0 || minute > 59 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59
|
||||
}
|
||||
|
||||
func (m *Manager) ValidateSchedule(startHour, startMinute, endHour, endMinute int) error {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
@@ -80,19 +81,24 @@ func (m Model) viewDependencyReview() string {
|
||||
}
|
||||
}
|
||||
|
||||
note := ""
|
||||
if dep.Name == "dms-greeter" {
|
||||
note = m.styles.Subtle.Render(" (selection replaces your current display manager)")
|
||||
}
|
||||
|
||||
var line string
|
||||
if i == m.selectedDep {
|
||||
line = fmt.Sprintf("▶ %s%s%-25s %s", reinstallMarker, variantMarker, dep.Name, status)
|
||||
if dep.Version != "" {
|
||||
line += fmt.Sprintf(" (%s)", dep.Version)
|
||||
}
|
||||
line = m.styles.SelectedOption.Render(line)
|
||||
line = m.styles.SelectedOption.Render(line) + note
|
||||
} else {
|
||||
line = fmt.Sprintf(" %s%s%-25s %s", reinstallMarker, variantMarker, dep.Name, status)
|
||||
if dep.Version != "" {
|
||||
line += fmt.Sprintf(" (%s)", dep.Version)
|
||||
}
|
||||
line = m.styles.Normal.Render(line)
|
||||
line = m.styles.Normal.Render(line) + note
|
||||
}
|
||||
|
||||
b.WriteString(line)
|
||||
@@ -115,6 +121,13 @@ func (m Model) updateDetectingDepsState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.state = StateError
|
||||
} else {
|
||||
m.dependencies = depsMsg.deps
|
||||
// dms-greeter is opt-in skipped by default
|
||||
for _, dep := range depsMsg.deps {
|
||||
if dep.Name == "dms-greeter" {
|
||||
m.disabledItems["dms-greeter"] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
m.state = StateDependencyReview
|
||||
}
|
||||
return m, m.listenForLogs()
|
||||
@@ -230,6 +243,41 @@ func (m Model) installPackages() tea.Cmd {
|
||||
// Convert installer messages to TUI messages
|
||||
go func() {
|
||||
for msg := range installerProgressChan {
|
||||
// Run optional greeter setup
|
||||
if msg.Phase == distros.PhaseComplete && msg.IsComplete && msg.Error == nil {
|
||||
greeterSelected := false
|
||||
for _, dep := range m.dependencies {
|
||||
if dep.Name == "dms-greeter" && !m.disabledItems["dms-greeter"] {
|
||||
greeterSelected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if greeterSelected {
|
||||
compositorName := "niri"
|
||||
if m.selectedWM == 1 {
|
||||
compositorName = "Hyprland"
|
||||
}
|
||||
m.packageProgressChan <- packageInstallProgressMsg{
|
||||
progress: 0.92,
|
||||
step: "Configuring DMS greeter...",
|
||||
logOutput: "Starting automated greeter setup...",
|
||||
}
|
||||
greeterLogFunc := func(line string) {
|
||||
m.packageProgressChan <- packageInstallProgressMsg{
|
||||
progress: 0.94,
|
||||
step: "Configuring DMS greeter...",
|
||||
logOutput: line,
|
||||
}
|
||||
}
|
||||
if err := greeter.AutoSetupGreeter(compositorName, m.sudoPassword, greeterLogFunc); err != nil {
|
||||
m.packageProgressChan <- packageInstallProgressMsg{
|
||||
progress: 0.96,
|
||||
step: "Greeter setup warning",
|
||||
logOutput: fmt.Sprintf("⚠ Greeter auto-setup warning (non-fatal): %v", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tuiMsg := packageInstallProgressMsg{
|
||||
progress: msg.Progress,
|
||||
step: msg.Step,
|
||||
|
||||
@@ -38,6 +38,22 @@ func XDGConfigHome() string {
|
||||
return filepath.Join(home, ".config")
|
||||
}
|
||||
|
||||
func EmacsConfigDir() string {
|
||||
home, _ := os.UserHomeDir()
|
||||
|
||||
emacsD := filepath.Join(home, ".emacs.d")
|
||||
if info, err := os.Stat(emacsD); err == nil && info.IsDir() {
|
||||
return emacsD
|
||||
}
|
||||
|
||||
xdgEmacs := filepath.Join(XDGConfigHome(), "emacs")
|
||||
if info, err := os.Stat(xdgEmacs); err == nil && info.IsDir() {
|
||||
return xdgEmacs
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func ExpandPath(path string) (string, error) {
|
||||
expanded := os.ExpandEnv(path)
|
||||
expanded = filepath.Clean(expanded)
|
||||
|
||||
@@ -864,10 +864,12 @@ func (p *NiriWritableProvider) formatRule(rule windowrules.WindowRule) string {
|
||||
|
||||
func formatSizeProperty(name, value string) string {
|
||||
parts := strings.SplitN(value, " ", 2)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Sprintf(" %s { }", name)
|
||||
if len(parts) == 2 {
|
||||
return fmt.Sprintf(" %s { %s %s; }", name, parts[0], parts[1])
|
||||
}
|
||||
sizeType := parts[0]
|
||||
sizeValue := parts[1]
|
||||
return fmt.Sprintf(" %s { %s %s; }", name, sizeType, sizeValue)
|
||||
// Bare number without type prefix — default to "fixed"
|
||||
if _, err := strconv.Atoi(value); err == nil {
|
||||
return fmt.Sprintf(" %s { fixed %s; }", name, value)
|
||||
}
|
||||
return fmt.Sprintf(" %s { }", name)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ Depends: ${misc:Depends},
|
||||
qml6-module-qtquick-templates,
|
||||
qml6-module-qtquick-window,
|
||||
qt6ct
|
||||
Suggests: cups-pk-helper
|
||||
Provides: dms
|
||||
Conflicts: dms
|
||||
Replaces: dms
|
||||
|
||||
9
distro/debian/dms-greeter/_service
Normal file
9
distro/debian/dms-greeter/_service
Normal file
@@ -0,0 +1,9 @@
|
||||
<services>
|
||||
<!-- Download dms-qml source tarball from GitHub releases (greeter + quickshell content) -->
|
||||
<service name="download_url">
|
||||
<param name="protocol">https</param>
|
||||
<param name="host">github.com</param>
|
||||
<param name="path">/AvengeMedia/DankMaterialShell/releases/download/v1.4.2/dms-qml.tar.gz</param>
|
||||
<param name="filename">dms-qml.tar.gz</param>
|
||||
</service>
|
||||
</services>
|
||||
6
distro/debian/dms-greeter/debian/changelog
Normal file
6
distro/debian/dms-greeter/debian/changelog
Normal file
@@ -0,0 +1,6 @@
|
||||
dms-greeter (1.4.2db8) unstable; urgency=medium
|
||||
|
||||
* Initial Debian OBS package
|
||||
* Port from Ubuntu/Fedora packaging
|
||||
|
||||
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 21 Feb 2026 00:00:00 +0000
|
||||
23
distro/debian/dms-greeter/debian/control
Normal file
23
distro/debian/dms-greeter/debian/control
Normal file
@@ -0,0 +1,23 @@
|
||||
Source: dms-greeter
|
||||
Section: x11
|
||||
Priority: optional
|
||||
Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
|
||||
Build-Depends: debhelper-compat (= 13)
|
||||
Standards-Version: 4.6.2
|
||||
Homepage: https://github.com/AvengeMedia/DankMaterialShell
|
||||
Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell
|
||||
Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
|
||||
|
||||
Package: dms-greeter
|
||||
Architecture: any
|
||||
Depends: ${misc:Depends},
|
||||
greetd,
|
||||
quickshell-git | quickshell
|
||||
Recommends: niri | hyprland | sway
|
||||
Description: DankMaterialShell greeter for greetd
|
||||
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
||||
inspired greeter interface built with Quickshell for Wayland compositors.
|
||||
.
|
||||
Supports multiple compositors including Niri, Hyprland, and Sway with automatic
|
||||
compositor detection and configuration. Features session selection, user
|
||||
authentication, and dynamic theming.
|
||||
27
distro/debian/dms-greeter/debian/copyright
Normal file
27
distro/debian/dms-greeter/debian/copyright
Normal file
@@ -0,0 +1,27 @@
|
||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: dms-greeter
|
||||
Upstream-Contact: Avenge Media LLC <AvengeMedia.US@gmail.com>
|
||||
Source: https://github.com/AvengeMedia/DankMaterialShell
|
||||
|
||||
Files: *
|
||||
Copyright: 2026 Avenge Media LLC
|
||||
License: MIT
|
||||
|
||||
License: MIT
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
108
distro/debian/dms-greeter/debian/postinst
Normal file
108
distro/debian/dms-greeter/debian/postinst
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
case "$1" in
|
||||
configure)
|
||||
# Create greeter user/group if they don't exist
|
||||
if ! getent group greeter >/dev/null; then
|
||||
addgroup --system greeter
|
||||
fi
|
||||
|
||||
if ! getent passwd greeter >/dev/null; then
|
||||
adduser --system --ingroup greeter --home /var/lib/greeter \
|
||||
--shell /bin/bash --gecos "System Greeter" greeter
|
||||
fi
|
||||
|
||||
if [ -d /var/cache/dms-greeter ]; then
|
||||
chown -R greeter:greeter /var/cache/dms-greeter 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ -d /var/lib/greeter ]; then
|
||||
chown -R greeter:greeter /var/lib/greeter 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Check and set graphical.target as default
|
||||
CURRENT_TARGET=$(systemctl get-default 2>/dev/null || echo "unknown")
|
||||
if [ "$CURRENT_TARGET" != "graphical.target" ]; then
|
||||
systemctl set-default graphical.target >/dev/null 2>&1 || true
|
||||
TARGET_STATUS="Set to graphical.target (was: $CURRENT_TARGET) ✓"
|
||||
else
|
||||
TARGET_STATUS="Already graphical.target ✓"
|
||||
fi
|
||||
|
||||
GREETD_CONFIG="/etc/greetd/config.toml"
|
||||
CONFIG_STATUS="Not modified (already configured)"
|
||||
|
||||
# Check if niri or hyprland exists
|
||||
COMPOSITOR="niri"
|
||||
if ! command -v niri >/dev/null 2>&1; then
|
||||
if command -v Hyprland >/dev/null 2>&1; then
|
||||
COMPOSITOR="hyprland"
|
||||
fi
|
||||
fi
|
||||
|
||||
# If config doesn't exist, create a default one
|
||||
if [ ! -f "$GREETD_CONFIG" ]; then
|
||||
mkdir -p /etc/greetd
|
||||
cat > "$GREETD_CONFIG" << 'GREETD_EOF'
|
||||
[terminal]
|
||||
vt = 1
|
||||
|
||||
[default_session]
|
||||
user = "greeter"
|
||||
command = "/usr/bin/dms-greeter --command COMPOSITOR_PLACEHOLDER"
|
||||
GREETD_EOF
|
||||
sed -i "s|COMPOSITOR_PLACEHOLDER|$COMPOSITOR|" "$GREETD_CONFIG"
|
||||
CONFIG_STATUS="Created new config with $COMPOSITOR ✓"
|
||||
elif ! grep -q "dms-greeter" "$GREETD_CONFIG"; then
|
||||
# Backup existing config
|
||||
BACKUP_FILE="${GREETD_CONFIG}.backup-$(date +%Y%m%d-%H%M%S)"
|
||||
cp "$GREETD_CONFIG" "$BACKUP_FILE" 2>/dev/null || true
|
||||
|
||||
# Update command in default_session section
|
||||
sed -i "/^\[default_session\]/,/^\[/ s|^command =.*|command = \"/usr/bin/dms-greeter --command $COMPOSITOR\"|" "$GREETD_CONFIG"
|
||||
sed -i '/^\[default_session\]/,/^\[/ s|^user =.*|user = "greeter"|' "$GREETD_CONFIG"
|
||||
CONFIG_STATUS="Updated existing config (backed up) with $COMPOSITOR ✓"
|
||||
fi
|
||||
|
||||
# Only show banner on initial install
|
||||
if [ -z "$2" ]; then
|
||||
cat << 'EOF'
|
||||
|
||||
=========================================================================
|
||||
DMS Greeter Installation Complete!
|
||||
=========================================================================
|
||||
|
||||
Status:
|
||||
EOF
|
||||
echo " ✓ Greetd config: $CONFIG_STATUS"
|
||||
echo " ✓ Default target: $TARGET_STATUS"
|
||||
cat << 'EOF'
|
||||
✓ Greeter user: Created
|
||||
✓ Greeter directories: /var/cache/dms-greeter, /var/lib/greeter
|
||||
|
||||
Next steps:
|
||||
|
||||
1. Enable the greeter:
|
||||
dms greeter enable
|
||||
(This will automatically disable conflicting display managers,
|
||||
set graphical.target, and enable greetd)
|
||||
|
||||
2. Sync your theme with the greeter (optional):
|
||||
dms greeter sync
|
||||
|
||||
3. Check your setup:
|
||||
dms greeter status
|
||||
|
||||
Ready to test? Run: sudo systemctl start greetd
|
||||
Documentation: https://danklinux.com/docs/dankgreeter/
|
||||
=========================================================================
|
||||
|
||||
EOF
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
#DEBHELPER#
|
||||
|
||||
exit 0
|
||||
14
distro/debian/dms-greeter/debian/postrm
Normal file
14
distro/debian/dms-greeter/debian/postrm
Normal file
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
case "$1" in
|
||||
purge)
|
||||
# Remove greeter cache directory on purge
|
||||
rm -rf /var/cache/dms-greeter 2>/dev/null || true
|
||||
|
||||
;;
|
||||
esac
|
||||
|
||||
#DEBHELPER#
|
||||
|
||||
exit 0
|
||||
48
distro/debian/dms-greeter/debian/rules
Normal file
48
distro/debian/dms-greeter/debian/rules
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/make -f
|
||||
|
||||
export DH_VERBOSE = 1
|
||||
|
||||
DEB_VERSION := $(shell dpkg-parsechangelog -S Version)
|
||||
UPSTREAM_VERSION := $(shell echo $(DEB_VERSION) | sed 's/-[^-]*$$//')
|
||||
|
||||
%:
|
||||
dh $@
|
||||
|
||||
override_dh_auto_build:
|
||||
: nothing to build, we use prebuilt tarball content
|
||||
|
||||
override_dh_auto_install:
|
||||
# Same pattern as dms: upstream from combined tarball (native format)
|
||||
# Build root is either . (we're inside dms-qml) or has dms-qml/ subdir
|
||||
SOURCE_DIR=""; \
|
||||
if [ -d dms-qml ]; then SOURCE_DIR="dms-qml"; \
|
||||
elif [ -f Modules/Greetd/assets/dms-greeter ]; then SOURCE_DIR="."; \
|
||||
fi; \
|
||||
if [ -n "$$SOURCE_DIR" ]; then \
|
||||
mkdir -p debian/dms-greeter/usr/share/quickshell/dms-greeter && \
|
||||
( cd $$SOURCE_DIR && tar cf - --exclude=debian . ) | \
|
||||
( cd debian/dms-greeter/usr/share/quickshell/dms-greeter && tar xf - ) && \
|
||||
install -Dm755 $$SOURCE_DIR/Modules/Greetd/assets/dms-greeter \
|
||||
debian/dms-greeter/usr/bin/dms-greeter && \
|
||||
install -Dm644 $$SOURCE_DIR/Modules/Greetd/README.md \
|
||||
debian/dms-greeter/usr/share/doc/dms-greeter/README.md && \
|
||||
install -Dm644 $$SOURCE_DIR/LICENSE \
|
||||
debian/dms-greeter/usr/share/doc/dms-greeter/LICENSE && \
|
||||
install -Dpm0644 $$SOURCE_DIR/systemd/tmpfiles-dms-greeter.conf \
|
||||
debian/dms-greeter/usr/lib/tmpfiles.d/dms-greeter.conf; \
|
||||
else \
|
||||
echo "ERROR: No upstream source (dms-qml or Modules/Greetd/assets/dms-greeter)!" && \
|
||||
echo "Contents of current directory:" && ls -la && exit 1; \
|
||||
fi
|
||||
|
||||
# Remove build and development files
|
||||
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/core
|
||||
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/distro
|
||||
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/.git*
|
||||
rm -f debian/dms-greeter/usr/share/quickshell/dms-greeter/.gitignore
|
||||
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/.github
|
||||
|
||||
override_dh_auto_clean:
|
||||
rm -rf dms-qml
|
||||
# When build root is dms-qml itself, we're inside it - nothing extra to remove
|
||||
dh_auto_clean
|
||||
1
distro/debian/dms-greeter/debian/source/format
Normal file
1
distro/debian/dms-greeter/debian/source/format
Normal file
@@ -0,0 +1 @@
|
||||
3.0 (native)
|
||||
1
distro/debian/dms-greeter/debian/source/options
Normal file
1
distro/debian/dms-greeter/debian/source/options
Normal file
@@ -0,0 +1 @@
|
||||
# OBS _service downloads dms-qml.tar.gz; no extra excludes needed
|
||||
@@ -28,6 +28,7 @@ Depends: ${misc:Depends},
|
||||
qml6-module-qtquick-templates,
|
||||
qml6-module-qtquick-window,
|
||||
qt6ct
|
||||
Suggests: cups-pk-helper
|
||||
Conflicts: dms-git
|
||||
Replaces: dms-git
|
||||
Description: DankMaterialShell - Modern Wayland Desktop Shell
|
||||
|
||||
@@ -37,6 +37,7 @@ Recommends: quickshell-git
|
||||
# Recommended system packages
|
||||
Recommends: NetworkManager
|
||||
Recommends: qt6-qtmultimedia
|
||||
Suggests: cups-pk-helper
|
||||
Suggests: qt6ct
|
||||
|
||||
%description
|
||||
|
||||
@@ -28,6 +28,7 @@ Recommends: danksearch
|
||||
Recommends: matugen
|
||||
Recommends: NetworkManager
|
||||
Recommends: qt6-qtmultimedia
|
||||
Suggests: cups-pk-helper
|
||||
Suggests: qt6ct
|
||||
|
||||
%description
|
||||
|
||||
@@ -25,6 +25,7 @@ Recommends: matugen
|
||||
Recommends: quickshell-git
|
||||
Recommends: NetworkManager
|
||||
Recommends: qt6-qtmultimedia
|
||||
Suggests: cups-pk-helper
|
||||
Suggests: qt6ct
|
||||
|
||||
Provides: dms
|
||||
|
||||
322
distro/opensuse/dms-greeter.spec
Normal file
322
distro/opensuse/dms-greeter.spec
Normal file
@@ -0,0 +1,322 @@
|
||||
# Spec for DMS Greeter - OpenSUSE/OBS
|
||||
|
||||
%global debug_package %{nil}
|
||||
%global version VERSION_PLACEHOLDER
|
||||
%global pkg_summary DankMaterialShell greeter for greetd
|
||||
|
||||
Name: dms-greeter
|
||||
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: greetd
|
||||
Requires: (quickshell-git or quickshell)
|
||||
Requires(post): /usr/sbin/useradd
|
||||
Requires(post): /usr/sbin/groupadd
|
||||
|
||||
Recommends: policycoreutils-python-utils
|
||||
Recommends: acl
|
||||
Suggests: niri
|
||||
Suggests: hyprland
|
||||
Suggests: sway
|
||||
|
||||
%description
|
||||
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
||||
inspired greeter interface built with Quickshell for Wayland compositors.
|
||||
|
||||
Supports multiple compositors including Niri, Hyprland, and Sway with automatic
|
||||
compositor detection and configuration. Features session selection, user
|
||||
authentication, and dynamic theming.
|
||||
|
||||
%prep
|
||||
%setup -q -c -n dms-qml
|
||||
|
||||
%build
|
||||
|
||||
%install
|
||||
# Install greeter files to shared data location
|
||||
install -dm755 %{buildroot}%{_datadir}/quickshell/dms-greeter
|
||||
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms-greeter/
|
||||
|
||||
install -Dm755 %{_builddir}/dms-qml/Modules/Greetd/assets/dms-greeter %{buildroot}%{_bindir}/dms-greeter
|
||||
|
||||
install -Dm644 %{_builddir}/dms-qml/Modules/Greetd/README.md %{buildroot}%{_docdir}/dms-greeter/README.md
|
||||
|
||||
install -Dpm0644 %{_builddir}/dms-qml/systemd/tmpfiles-dms-greeter.conf %{buildroot}%{_tmpfilesdir}/dms-greeter.conf
|
||||
|
||||
install -Dm644 %{_builddir}/dms-qml/LICENSE %{buildroot}%{_docdir}/dms-greeter/LICENSE
|
||||
|
||||
install -dm755 %{buildroot}%{_sharedstatedir}/greeter
|
||||
|
||||
# Remove build and development files
|
||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms-greeter/.git*
|
||||
rm -f %{buildroot}%{_datadir}/quickshell/dms-greeter/.gitignore
|
||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms-greeter/.github
|
||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms-greeter/distro
|
||||
|
||||
%posttrans
|
||||
if [ -d "%{_sysconfdir}/xdg/quickshell/dms-greeter" ]; then
|
||||
rmdir "%{_sysconfdir}/xdg/quickshell/dms-greeter" 2>/dev/null || true
|
||||
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
|
||||
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
%files
|
||||
%dir %{_docdir}/dms-greeter
|
||||
%license %{_docdir}/dms-greeter/LICENSE
|
||||
%doc %{_docdir}/dms-greeter/README.md
|
||||
%{_bindir}/dms-greeter
|
||||
%dir %{_datadir}/quickshell
|
||||
%{_datadir}/quickshell/dms-greeter/
|
||||
%{_tmpfilesdir}/%{name}.conf
|
||||
|
||||
%pre
|
||||
# Create greeter user/group if they don't exist
|
||||
getent group greeter >/dev/null || groupadd -r greeter
|
||||
getent passwd greeter >/dev/null || \
|
||||
useradd -r -g greeter -d %{_sharedstatedir}/greeter -s /bin/bash \
|
||||
-c "System Greeter" greeter
|
||||
exit 0
|
||||
|
||||
%post
|
||||
# SELinux contexts (no-op on OpenSUSE - semanage/restorecon not present)
|
||||
if [ -x /usr/sbin/semanage ] && [ -x /usr/sbin/restorecon ]; then
|
||||
semanage fcontext -a -t bin_t '%{_bindir}/dms-greeter' >/dev/null 2>&1 || true
|
||||
restorecon %{_bindir}/dms-greeter >/dev/null 2>&1 || true
|
||||
semanage fcontext -a -t user_home_dir_t '%{_sharedstatedir}/greeter(/.*)?' >/dev/null 2>&1 || true
|
||||
restorecon -R %{_sharedstatedir}/greeter >/dev/null 2>&1 || true
|
||||
semanage fcontext -a -t cache_home_t '%{_localstatedir}/cache/dms-greeter(/.*)?' >/dev/null 2>&1 || true
|
||||
restorecon -R %{_localstatedir}/cache/dms-greeter >/dev/null 2>&1 || true
|
||||
semanage fcontext -a -t usr_t '%{_datadir}/quickshell/dms-greeter(/.*)?' >/dev/null 2>&1 || true
|
||||
restorecon -R %{_datadir}/quickshell/dms-greeter >/dev/null 2>&1 || true
|
||||
restorecon %{_sysconfdir}/pam.d/greetd >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Resolve greeter runtime account/group for distro differences
|
||||
GREETER_USER="greeter"
|
||||
for candidate in greeter greetd _greeter; do
|
||||
if getent passwd "$candidate" >/dev/null 2>&1; then
|
||||
GREETER_USER="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
GREETER_GROUP="$GREETER_USER"
|
||||
if ! getent group "$GREETER_GROUP" >/dev/null 2>&1; then
|
||||
for candidate in greeter greetd _greeter; do
|
||||
if getent group "$candidate" >/dev/null 2>&1; then
|
||||
GREETER_GROUP="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Ensure proper ownership of greeter directories
|
||||
chown -R "$GREETER_USER:$GREETER_GROUP" %{_localstatedir}/cache/dms-greeter 2>/dev/null || true
|
||||
chown -R "$GREETER_USER:$GREETER_GROUP" %{_sharedstatedir}/greeter 2>/dev/null || true
|
||||
|
||||
# Verify PAM configuration
|
||||
PAM_CONFIG="/etc/pam.d/greetd"
|
||||
write_greetd_pam_config() {
|
||||
# openSUSE and Debian families usually expose PAM stacks as common-*
|
||||
if [ -f /etc/pam.d/common-auth ] && [ -f /etc/pam.d/common-account ] && [ -f /etc/pam.d/common-password ] && [ -f /etc/pam.d/common-session ]; then
|
||||
cat > "$PAM_CONFIG" << 'PAM_EOF'
|
||||
#%PAM-1.0
|
||||
auth include common-auth
|
||||
account required pam_nologin.so
|
||||
account include common-account
|
||||
password include common-password
|
||||
session required pam_loginuid.so
|
||||
session optional pam_keyinit.so force revoke
|
||||
session include common-session
|
||||
PAM_EOF
|
||||
return
|
||||
fi
|
||||
|
||||
# Fedora/RHEL style system-auth/postlogin stack
|
||||
if [ -f /etc/pam.d/system-auth ]; then
|
||||
if [ -f /etc/pam.d/postlogin ]; then
|
||||
cat > "$PAM_CONFIG" << 'PAM_EOF'
|
||||
#%PAM-1.0
|
||||
auth substack system-auth
|
||||
auth include postlogin
|
||||
account required pam_nologin.so
|
||||
account include system-auth
|
||||
password include system-auth
|
||||
session required pam_loginuid.so
|
||||
session optional pam_keyinit.so force revoke
|
||||
session include system-auth
|
||||
session include postlogin
|
||||
PAM_EOF
|
||||
else
|
||||
cat > "$PAM_CONFIG" << 'PAM_EOF'
|
||||
#%PAM-1.0
|
||||
auth include system-auth
|
||||
account required pam_nologin.so
|
||||
account include system-auth
|
||||
password include system-auth
|
||||
session required pam_loginuid.so
|
||||
session optional pam_keyinit.so force revoke
|
||||
session include system-auth
|
||||
PAM_EOF
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
# Last-resort conservative fallback
|
||||
cat > "$PAM_CONFIG" << 'PAM_EOF'
|
||||
#%PAM-1.0
|
||||
auth required pam_unix.so nullok
|
||||
account required pam_unix.so
|
||||
password required pam_unix.so nullok sha512
|
||||
session required pam_unix.so
|
||||
PAM_EOF
|
||||
}
|
||||
|
||||
if [ ! -f "$PAM_CONFIG" ]; then
|
||||
write_greetd_pam_config
|
||||
chmod 644 "$PAM_CONFIG"
|
||||
[ "$1" -eq 1 ] && echo "Created PAM configuration for greetd"
|
||||
else
|
||||
NEEDS_PAM_UPDATE=0
|
||||
if grep -q "common-auth" "$PAM_CONFIG"; then
|
||||
if [ ! -f /etc/pam.d/common-auth ]; then
|
||||
NEEDS_PAM_UPDATE=1
|
||||
fi
|
||||
elif grep -q "system-auth" "$PAM_CONFIG"; then
|
||||
if [ ! -f /etc/pam.d/system-auth ]; then
|
||||
NEEDS_PAM_UPDATE=1
|
||||
fi
|
||||
else
|
||||
NEEDS_PAM_UPDATE=1
|
||||
fi
|
||||
|
||||
if [ "$NEEDS_PAM_UPDATE" -eq 1 ]; then
|
||||
cp "$PAM_CONFIG" "$PAM_CONFIG.backup-dms-greeter"
|
||||
write_greetd_pam_config
|
||||
chmod 644 "$PAM_CONFIG"
|
||||
[ "$1" -eq 1 ] && echo "Updated PAM configuration (old config backed up to $PAM_CONFIG.backup-dms-greeter)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Auto-configure greetd config
|
||||
GREETD_CONFIG="/etc/greetd/config.toml"
|
||||
CONFIG_STATUS="Not modified (already configured)"
|
||||
|
||||
COMPOSITOR=""
|
||||
for candidate in niri Hyprland sway; do
|
||||
if command -v "$candidate" >/dev/null 2>&1; then
|
||||
case "$candidate" in
|
||||
Hyprland)
|
||||
COMPOSITOR="hyprland"
|
||||
;;
|
||||
*)
|
||||
COMPOSITOR="$candidate"
|
||||
;;
|
||||
esac
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ! -f "$GREETD_CONFIG" ]; then
|
||||
mkdir -p /etc/greetd
|
||||
if [ -n "$COMPOSITOR" ]; then
|
||||
cat > "$GREETD_CONFIG" << 'GREETD_EOF'
|
||||
[terminal]
|
||||
vt = 1
|
||||
|
||||
[default_session]
|
||||
user = "GREETER_USER_PLACEHOLDER"
|
||||
command = "/usr/bin/dms-greeter --command COMPOSITOR_PLACEHOLDER"
|
||||
GREETD_EOF
|
||||
sed -i "s|GREETER_USER_PLACEHOLDER|$GREETER_USER|" "$GREETD_CONFIG"
|
||||
sed -i "s|COMPOSITOR_PLACEHOLDER|$COMPOSITOR|" "$GREETD_CONFIG"
|
||||
CONFIG_STATUS="Created new config with $COMPOSITOR ✓"
|
||||
else
|
||||
cat > "$GREETD_CONFIG" << 'GREETD_EOF'
|
||||
[terminal]
|
||||
vt = 1
|
||||
|
||||
[default_session]
|
||||
user = "GREETER_USER_PLACEHOLDER"
|
||||
command = "agreety --cmd /bin/login"
|
||||
GREETD_EOF
|
||||
sed -i "s|GREETER_USER_PLACEHOLDER|$GREETER_USER|" "$GREETD_CONFIG"
|
||||
CONFIG_STATUS="Created safe fallback config (no supported compositor detected)"
|
||||
fi
|
||||
elif ! grep -q "dms-greeter" "$GREETD_CONFIG"; then
|
||||
if [ -n "$COMPOSITOR" ]; then
|
||||
BACKUP_FILE="${GREETD_CONFIG}.backup-$(date +%%Y%%m%%d-%%H%%M%%S)"
|
||||
cp "$GREETD_CONFIG" "$BACKUP_FILE" 2>/dev/null || true
|
||||
sed -i "/^\[default_session\]/,/^\[/ s|^command =.*|command = \"/usr/bin/dms-greeter --command $COMPOSITOR\"|" "$GREETD_CONFIG"
|
||||
sed -i "/^\[default_session\]/,/^\[/ s|^user =.*|user = \"$GREETER_USER\"|" "$GREETD_CONFIG"
|
||||
CONFIG_STATUS="Updated existing config (backed up) with $COMPOSITOR ✓"
|
||||
else
|
||||
CONFIG_STATUS="Skipped dms-greeter command update (no supported compositor detected)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Set graphical.target as default
|
||||
CURRENT_TARGET=$(systemctl get-default 2>/dev/null || echo "unknown")
|
||||
if [ "$CURRENT_TARGET" != "graphical.target" ]; then
|
||||
systemctl set-default graphical.target >/dev/null 2>&1 || true
|
||||
TARGET_STATUS="Set to graphical.target (was: $CURRENT_TARGET) ✓"
|
||||
else
|
||||
TARGET_STATUS="Already graphical.target ✓"
|
||||
fi
|
||||
|
||||
if [ "$1" -eq 1 ]; then
|
||||
cat << 'EOF'
|
||||
|
||||
=========================================================================
|
||||
DMS Greeter Installation Complete!
|
||||
=========================================================================
|
||||
|
||||
Status:
|
||||
EOF
|
||||
echo " ✓ Greetd config: $CONFIG_STATUS"
|
||||
echo " ✓ Default target: $TARGET_STATUS"
|
||||
cat << 'EOF'
|
||||
✓ Greeter user: Created
|
||||
✓ Greeter directories: /var/cache/dms-greeter, /var/lib/greeter
|
||||
✓ SELinux contexts: Applied (if applicable)
|
||||
|
||||
Next steps:
|
||||
|
||||
1. Enable the greeter:
|
||||
dms greeter enable
|
||||
|
||||
2. Sync your theme with the greeter (optional):
|
||||
dms greeter sync
|
||||
|
||||
3. Check your setup:
|
||||
dms greeter status
|
||||
|
||||
Ready to test? Run: sudo systemctl start greetd
|
||||
Documentation: https://danklinux.com/docs/dankgreeter/
|
||||
=========================================================================
|
||||
|
||||
EOF
|
||||
fi
|
||||
|
||||
%postun
|
||||
if [ "$1" -eq 0 ] && [ -x /usr/sbin/semanage ]; then
|
||||
semanage fcontext -d '%{_bindir}/dms-greeter' 2>/dev/null || true
|
||||
semanage fcontext -d '%{_sharedstatedir}/greeter(/.*)?' 2>/dev/null || true
|
||||
semanage fcontext -d '%{_localstatedir}/cache/dms-greeter(/.*)?' 2>/dev/null || true
|
||||
semanage fcontext -d '%{_datadir}/quickshell/dms-greeter(/.*)?' 2>/dev/null || true
|
||||
fi
|
||||
|
||||
%changelog
|
||||
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER
|
||||
- Stable release VERSION_PLACEHOLDER
|
||||
- Initial OpenSUSE/OBS port from Fedora
|
||||
@@ -27,6 +27,7 @@ Recommends: danksearch
|
||||
Recommends: matugen
|
||||
Recommends: NetworkManager
|
||||
Recommends: qt6-qtmultimedia
|
||||
Suggests: cups-pk-helper
|
||||
Suggests: qt6ct
|
||||
|
||||
%description
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
OBS_BASE_PROJECT="home:AvengeMedia"
|
||||
OBS_BASE="$HOME/.cache/osc-checkouts"
|
||||
|
||||
ALL_PACKAGES=(dms dms-git)
|
||||
ALL_PACKAGES=(dms dms-git dms-greeter)
|
||||
|
||||
REPOS=("Debian_13" "openSUSE_Tumbleweed" "16.0")
|
||||
ARCHES=("x86_64" "aarch64")
|
||||
@@ -41,6 +41,9 @@ for pkg in "${PACKAGES[@]}"; do
|
||||
dms-git)
|
||||
PROJECT="$OBS_BASE_PROJECT:dms-git"
|
||||
;;
|
||||
dms-greeter)
|
||||
PROJECT="$OBS_BASE_PROJECT:danklinux"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown package '$pkg'"
|
||||
continue
|
||||
@@ -74,11 +77,15 @@ for pkg in "${PACKAGES[@]}"; do
|
||||
COLOR="\033[0;32m" # Green
|
||||
SYMBOL="✅"
|
||||
;;
|
||||
failed)
|
||||
failed|broken|broken*)
|
||||
COLOR="\033[0;31m" # Red
|
||||
SYMBOL="❌"
|
||||
FAILED_BUILDS+=("$repo $arch")
|
||||
;;
|
||||
blocked)
|
||||
COLOR="\033[0;33m" # Yellow
|
||||
SYMBOL="⏸️"
|
||||
;;
|
||||
unresolvable)
|
||||
COLOR="\033[0;33m" # Yellow
|
||||
SYMBOL="⚠️"
|
||||
|
||||
@@ -68,13 +68,14 @@ fi
|
||||
|
||||
OBS_BASE_PROJECT="home:AvengeMedia"
|
||||
OBS_BASE="$HOME/.cache/osc-checkouts"
|
||||
AVAILABLE_PACKAGES=(dms dms-git)
|
||||
AVAILABLE_PACKAGES=(dms dms-git dms-greeter)
|
||||
|
||||
if [[ -z "$PACKAGE" ]]; then
|
||||
echo "Available packages:"
|
||||
echo ""
|
||||
echo " 1. dms - Stable DMS"
|
||||
echo " 2. dms-git - Nightly DMS"
|
||||
echo " 3. dms-greeter - DMS greeter for greetd"
|
||||
echo " a. all"
|
||||
echo ""
|
||||
read -r -p "Select package (1-${#AVAILABLE_PACKAGES[@]}, a): " selection
|
||||
@@ -141,7 +142,12 @@ check_obs_version_exists() {
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
echo "⚠️ Could not fetch OBS spec (API may be unavailable), proceeding anyway"
|
||||
# Empty/invalid response: expected on first upload (no spec on server yet), or actual API failure
|
||||
if [[ -z "$OBS_SPEC" ]]; then
|
||||
echo " No existing spec on OBS (first upload?) - proceeding"
|
||||
else
|
||||
echo "⚠️ Could not fetch OBS spec (API may be unavailable), proceeding anyway"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
return 1
|
||||
@@ -168,6 +174,22 @@ update_debian_dms_service() {
|
||||
sed -i "s|/releases/download/v[0-9][^\"]*/dms-distropkg-arm64\.gz|/releases/download/v${base_version}/dms-distropkg-arm64.gz|" "$service_path"
|
||||
}
|
||||
|
||||
update_debian_dms_greeter_service() {
|
||||
local service_path="$1"
|
||||
if [[ -z "$service_path" || ! -f "$service_path" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ -z "$CHANGELOG_VERSION" ]]; then
|
||||
return 0
|
||||
fi
|
||||
local base_version
|
||||
base_version=$(echo "$CHANGELOG_VERSION" | sed -E 's/^([0-9]+(\.[0-9]+)*).*/\1/')
|
||||
if [[ -z "$base_version" ]]; then
|
||||
return 0
|
||||
fi
|
||||
sed -i "s|/releases/download/v[0-9][^\"]*/dms-qml\.tar\.gz|/releases/download/v${base_version}/dms-qml.tar.gz|" "$service_path"
|
||||
}
|
||||
|
||||
update_opensuse_git_spec() {
|
||||
local spec_path="$1"
|
||||
if [[ -z "$spec_path" || ! -f "$spec_path" ]]; then
|
||||
@@ -248,6 +270,9 @@ dms)
|
||||
dms-git)
|
||||
PROJECT="dms-git"
|
||||
;;
|
||||
dms-greeter)
|
||||
PROJECT="danklinux"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown package '$PACKAGE'"
|
||||
exit 1
|
||||
@@ -329,9 +354,11 @@ if [[ -d "distro/debian/$PACKAGE/debian" ]]; then
|
||||
echo " - Applied rebuild suffix: $CHANGELOG_VERSION"
|
||||
fi
|
||||
|
||||
# Keep Debian dms _service in sync with changelog version
|
||||
# Keep Debian _service in sync with changelog version
|
||||
if [[ "$PACKAGE" == "dms" ]] && [[ -f "distro/debian/$PACKAGE/_service" ]]; then
|
||||
update_debian_dms_service "distro/debian/$PACKAGE/_service"
|
||||
elif [[ "$PACKAGE" == "dms-greeter" ]] && [[ -f "distro/debian/$PACKAGE/_service" ]]; then
|
||||
update_debian_dms_greeter_service "distro/debian/$PACKAGE/_service"
|
||||
fi
|
||||
|
||||
# Check if this version already exists in OBS
|
||||
@@ -341,6 +368,12 @@ if [[ -d "distro/debian/$PACKAGE/debian" ]]; then
|
||||
if [[ "$PACKAGE" == *"-git" ]]; then
|
||||
echo "==> Error: This commit is already uploaded to OBS"
|
||||
echo " The same git commit ($(echo "$CHANGELOG_VERSION" | grep -oP '[a-f0-9]{8}' | tail -1)) already exists on OBS."
|
||||
if [[ -n "${GITHUB_ACTIONS:-}" ]] || [[ -n "${CI:-}" ]]; then
|
||||
echo " CI run detected: skipping upload as a no-op (already up to date)."
|
||||
echo " If you need to force rebuild this same commit, set REBUILD_RELEASE (e.g. 2, 3, ...)."
|
||||
echo "✓ Exiting gracefully (no changes needed)"
|
||||
exit 0
|
||||
fi
|
||||
echo " To rebuild the same commit, specify a rebuild number:"
|
||||
echo " ./distro/scripts/obs-upload.sh $PACKAGE 2"
|
||||
echo " ./distro/scripts/obs-upload.sh $PACKAGE 3"
|
||||
@@ -379,6 +412,13 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ -f "distro/opensuse/$PACKAGE.spec" ]];
|
||||
|
||||
if [[ "$PACKAGE" == *"-git" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then
|
||||
update_opensuse_git_spec "$WORK_DIR/$PACKAGE.spec"
|
||||
elif [[ "$PACKAGE" == "dms-greeter" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then
|
||||
DMS_GREETER_BASE_VERSION=$(echo "$CHANGELOG_VERSION" | sed -E 's/^([0-9]+(\.[0-9]+)*).*/\1/')
|
||||
DMS_GREETER_RELEASE=$(echo "$CHANGELOG_VERSION" | sed -E 's/.*db([0-9]+)$/\1/' || echo "1")
|
||||
CHANGELOG_DATE=$(date '+%a %b %d %Y')
|
||||
sed -i "s/VERSION_PLACEHOLDER/${DMS_GREETER_BASE_VERSION}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||
sed -i "s/RELEASE_PLACEHOLDER/${DMS_GREETER_RELEASE}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||
fi
|
||||
|
||||
if [[ -f "$WORK_DIR/.osc/$PACKAGE.spec" ]]; then
|
||||
@@ -444,6 +484,24 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ "$UPLOAD_DEBIAN" == false ]] && [[ -f
|
||||
fi
|
||||
fi
|
||||
|
||||
# For dms-greeter: download dms-qml.tar.gz from _service URL
|
||||
if [[ -z "${SOURCE_DIR:-}" ]] && [[ "$PACKAGE" == "dms-greeter" ]] && [[ -f "distro/debian/$PACKAGE/_service" ]]; then
|
||||
DMS_GREETER_URL=$(grep -A 5 'name="download_url"' "distro/debian/$PACKAGE/_service" | grep "path" | sed 's/.*<param name="path">\(.*\)<\/param>.*/\1/' | head -1)
|
||||
if [[ -n "$DMS_GREETER_URL" ]]; then
|
||||
DMS_GREETER_FULL_URL="https://github.com${DMS_GREETER_URL}"
|
||||
echo " Downloading dms-greeter source from: $DMS_GREETER_FULL_URL"
|
||||
if wget -q -O "$TEMP_DIR/dms-qml.tar.gz" "$DMS_GREETER_FULL_URL" 2>/dev/null || \
|
||||
curl -L -f -s -o "$TEMP_DIR/dms-qml.tar.gz" "$DMS_GREETER_FULL_URL" 2>/dev/null; then
|
||||
cd "$TEMP_DIR"
|
||||
tar -xzf dms-qml.tar.gz
|
||||
if [[ -f "Modules/Greetd/assets/dms-greeter" ]]; then
|
||||
SOURCE_DIR="$TEMP_DIR"
|
||||
fi
|
||||
cd "$REPO_ROOT"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$SOURCE_DIR" && -d "$SOURCE_DIR" ]]; then
|
||||
SOURCE0=$(grep "^Source0:" "distro/opensuse/$PACKAGE.spec" | awk '{print $2}' | head -1)
|
||||
|
||||
@@ -452,6 +510,15 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ "$UPLOAD_DEBIAN" == false ]] && [[ -f
|
||||
cd "$OBS_TARBALL_DIR"
|
||||
|
||||
case "$PACKAGE" in
|
||||
dms-greeter)
|
||||
EXPECTED_DIR="dms-qml"
|
||||
echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)"
|
||||
mkdir -p "$EXPECTED_DIR"
|
||||
cp -a "$SOURCE_DIR"/. "$EXPECTED_DIR/"
|
||||
tar -czf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
rm -rf "$EXPECTED_DIR"
|
||||
echo " Created $SOURCE0 ($(stat -c%s "$WORK_DIR/$SOURCE0" 2>/dev/null || echo 0) bytes)"
|
||||
;;
|
||||
dms)
|
||||
DMS_VERSION=$(grep "^Version:" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec" | sed 's/^Version:[[:space:]]*//' | head -1)
|
||||
EXPECTED_DIR="DankMaterialShell-${DMS_VERSION}"
|
||||
@@ -584,6 +651,17 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
|
||||
if [[ -z "$SOURCE_DIR" ]]; then
|
||||
SOURCE_DIR=$(find . -maxdepth 1 -type d ! -name "." | head -1)
|
||||
fi
|
||||
# dms-qml.tar.gz extracts flat (quickshell contents, no top-level dir)
|
||||
# Create dms-qml wrapper so combined tarball has correct top-level dir (like dms)
|
||||
if [[ "$PACKAGE" == "dms-greeter" ]] && [[ -f "Modules/Greetd/assets/dms-greeter" ]]; then
|
||||
mkdir -p dms-qml
|
||||
for f in *; do
|
||||
if [[ -e "$f" && "$f" != "dms-qml" && "$f" != "source-archive" ]]; then
|
||||
mv "$f" dms-qml/ 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
SOURCE_DIR="dms-qml"
|
||||
fi
|
||||
if [[ -z "$SOURCE_DIR" || ! -d "$SOURCE_DIR" ]]; then
|
||||
echo "Error: Failed to extract source archive or find source directory"
|
||||
echo "Contents of $TEMP_DIR:"
|
||||
@@ -660,6 +738,21 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
|
||||
cd "$OBS_TARBALL_DIR"
|
||||
|
||||
case "$PACKAGE" in
|
||||
dms-greeter)
|
||||
EXPECTED_DIR="dms-qml"
|
||||
echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)"
|
||||
mkdir -p "$EXPECTED_DIR"
|
||||
cp -a "$SOURCE_DIR"/. "$EXPECTED_DIR/"
|
||||
if [[ "$SOURCE0" == *.tar.xz ]]; then
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
elif [[ "$SOURCE0" == *.tar.bz2 ]]; then
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
else
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
fi
|
||||
rm -rf "$EXPECTED_DIR"
|
||||
echo " Created $SOURCE0 ($(stat -c%s "$WORK_DIR/$SOURCE0" 2>/dev/null || echo 0) bytes)"
|
||||
;;
|
||||
dms)
|
||||
DMS_VERSION=$(grep "^Version:" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec" | sed 's/^Version:[[:space:]]*//' | head -1)
|
||||
EXPECTED_DIR="DankMaterialShell-${DMS_VERSION}"
|
||||
@@ -709,10 +802,17 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
|
||||
echo " - OpenSUSE source tarballs created"
|
||||
fi
|
||||
|
||||
# Copy and update OpenSUSE spec file with the correct version (for -git packages)
|
||||
# Copy and update OpenSUSE spec file with the correct version
|
||||
cp "distro/opensuse/$PACKAGE.spec" "$WORK_DIR/"
|
||||
if [[ "$PACKAGE" == *"-git" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then
|
||||
update_opensuse_git_spec "$WORK_DIR/$PACKAGE.spec"
|
||||
elif [[ "$PACKAGE" == "dms-greeter" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then
|
||||
DMS_GREETER_BASE_VERSION=$(echo "$CHANGELOG_VERSION" | sed -E 's/^([0-9]+(\.[0-9]+)*).*/\1/')
|
||||
DMS_GREETER_RELEASE=$(echo "$CHANGELOG_VERSION" | sed -E 's/.*db([0-9]+)$/\1/' || echo "1")
|
||||
CHANGELOG_DATE=$(date '+%a %b %d %Y')
|
||||
sed -i "s/VERSION_PLACEHOLDER/${DMS_GREETER_BASE_VERSION}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||
sed -i "s/RELEASE_PLACEHOLDER/${DMS_GREETER_RELEASE}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -839,8 +939,48 @@ EOF
|
||||
echo " - Quilt format detected: creating debian.tar.gz"
|
||||
tar -czf "$WORK_DIR/debian.tar.gz" -C "distro/debian/$PACKAGE" debian/
|
||||
|
||||
# For dms-greeter: create orig tarball so Debian build gets upstream (OBS only passes .dsc Files to Debian)
|
||||
DSC_FILES_DEBIAN=""
|
||||
if [[ "$PACKAGE" == "dms-greeter" ]]; then
|
||||
UPSTREAM_VER=$(echo "$VERSION" | sed 's/-[^-]*$//')
|
||||
ORIG_TARBALL="${PACKAGE}_${UPSTREAM_VER}.orig.tar.gz"
|
||||
ORIG_DIR="${PACKAGE}-${UPSTREAM_VER}"
|
||||
|
||||
if [[ -f "distro/debian/$PACKAGE/_service" ]] && grep -q "download_url" "distro/debian/$PACKAGE/_service"; then
|
||||
DG_TEMP=$(mktemp -d)
|
||||
DMS_GREETER_PATH=$(grep -A 5 'name="download_url"' "distro/debian/$PACKAGE/_service" | grep "path" | sed 's/.*<param name="path">\(.*\)<\/param>.*/\1/' | head -1)
|
||||
if [[ -n "$DMS_GREETER_PATH" ]]; then
|
||||
DG_URL="https://github.com${DMS_GREETER_PATH}"
|
||||
echo " - Downloading dms-greeter source for orig tarball: $DG_URL"
|
||||
if wget -q -O "$DG_TEMP/dms-qml.tar.gz" "$DG_URL" 2>/dev/null || curl -L -f -s -o "$DG_TEMP/dms-qml.tar.gz" "$DG_URL" 2>/dev/null; then
|
||||
( cd "$DG_TEMP" && tar --no-same-owner -xzf dms-qml.tar.gz && mkdir -p "$ORIG_DIR" && \
|
||||
for f in *; do [[ "$f" != "dms-qml.tar.gz" && "$f" != "$ORIG_DIR" ]] && mv "$f" "$ORIG_DIR/"; done )
|
||||
if [[ -d "$DG_TEMP/$ORIG_DIR/Modules" ]] || [[ -f "$DG_TEMP/$ORIG_DIR/LICENSE" ]]; then
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$ORIG_TARBALL" -C "$DG_TEMP" "$ORIG_DIR"
|
||||
ORIG_MD5=$(md5sum "$WORK_DIR/$ORIG_TARBALL" | cut -d' ' -f1)
|
||||
ORIG_SIZE=$(stat -c%s "$WORK_DIR/$ORIG_TARBALL" 2>/dev/null || stat -f%z "$WORK_DIR/$ORIG_TARBALL" 2>/dev/null)
|
||||
DSC_FILES_DEBIAN=" $ORIG_MD5 $ORIG_SIZE $ORIG_TARBALL
|
||||
"
|
||||
echo " - Created $ORIG_TARBALL for Debian orig"
|
||||
fi
|
||||
rm -rf "$DG_TEMP"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
DEBIAN_MD5=$(md5sum "$WORK_DIR/debian.tar.gz" | cut -d' ' -f1)
|
||||
DEBIAN_SIZE=$(stat -c%s "$WORK_DIR/debian.tar.gz" 2>/dev/null || stat -f%z "$WORK_DIR/debian.tar.gz" 2>/dev/null)
|
||||
|
||||
echo " - Generating $PACKAGE.dsc for quilt format"
|
||||
cat >"$WORK_DIR/$PACKAGE.dsc" <<EOF
|
||||
# debtransform: DEBTRANSFORM-TAR = orig (upstream), DEBTRANSFORM-FILES-TAR = debian archive
|
||||
DEBTRANSFORM_EXTRA=""
|
||||
if [[ -n "$DSC_FILES_DEBIAN" ]] && [[ -n "$ORIG_TARBALL" ]]; then
|
||||
DEBTRANSFORM_EXTRA="DEBTRANSFORM-TAR: $ORIG_TARBALL
|
||||
DEBTRANSFORM-FILES-TAR: debian.tar.gz
|
||||
"
|
||||
fi
|
||||
cat >"$WORK_DIR/$PACKAGE.dsc" <<DSCEOF
|
||||
Format: 3.0 (quilt)
|
||||
Source: $PACKAGE
|
||||
Binary: $PACKAGE
|
||||
@@ -848,10 +988,9 @@ Architecture: any
|
||||
Version: $VERSION
|
||||
Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
|
||||
Build-Depends: debhelper-compat (= 13), wget, gzip
|
||||
DEBTRANSFORM-TAR: debian.tar.gz
|
||||
Files:
|
||||
00000000000000000000000000000000 1 debian.tar.gz
|
||||
EOF
|
||||
${DEBTRANSFORM_EXTRA}Files:${DSC_FILES_DEBIAN}
|
||||
$DEBIAN_MD5 $DEBIAN_SIZE debian.tar.gz
|
||||
DSCEOF
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
@@ -882,6 +1021,13 @@ if [[ -n "$OBS_FILES" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Keep current orig tarball for dms-greeter (Debian 3.0 quilt needs it)
|
||||
UPSTREAM_VER_CLEAN=$(echo "$CHANGELOG_VERSION" | sed 's/-[^-]*$//' 2>/dev/null)
|
||||
if [[ "$PACKAGE" == "dms-greeter" ]] && [[ "$old_file" == "${PACKAGE}_${UPSTREAM_VER_CLEAN}.orig.tar.gz" ]]; then
|
||||
echo " - Keeping orig tarball: $old_file"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$old_file" == "${PACKAGE}-source.tar.gz" ]]; then
|
||||
echo " - Keeping source tarball: $old_file"
|
||||
continue
|
||||
|
||||
@@ -28,8 +28,17 @@ override_dh_auto_build:
|
||||
# Launchpad build environment has no internet access
|
||||
test -d dms-git-repo || (echo "ERROR: dms-git-repo directory not found!" && exit 1)
|
||||
|
||||
# Patch go.mod to use Go 1.24 base version (Ubuntu has 1.24.4, project requires 1.24.6)
|
||||
sed -i 's/^go 1\.24\.[0-9]*/go 1.24/' dms-git-repo/core/go.mod
|
||||
# Patch go.mod for Launchpad: align go directive w/latest Go toolchain
|
||||
GO_VERSION=$$(go env GOVERSION | sed -E 's/^go([0-9]+\.[0-9]+).*/\1/'); \
|
||||
if [ -n "$$GO_VERSION" ]; then \
|
||||
sed -E -i "s/^go 1\.[0-9]+(\.[0-9]+)?/go $$GO_VERSION/" dms-git-repo/core/go.mod; \
|
||||
if [ -f dms-git-repo/core/vendor/modules.txt ]; then \
|
||||
sed -E -i "s/^(## explicit; go )1\.[0-9]+(\.[0-9]+)?$$/\1$$GO_VERSION/" dms-git-repo/core/vendor/modules.txt; \
|
||||
fi; \
|
||||
else \
|
||||
echo "Warning: Could not detect Go version, leaving go.mod go directive unchanged"; \
|
||||
fi
|
||||
sed -E -i '/^toolchain go[0-9.]+$$/d' dms-git-repo/core/go.mod
|
||||
|
||||
# Build dms-cli from source
|
||||
# Detect architecture
|
||||
|
||||
6
flake.lock
generated
6
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1769018530,
|
||||
"narHash": "sha256-MJ27Cy2NtBEV5tsK+YraYr2g851f3Fl1LpNHDzDX15c=",
|
||||
"lastModified": 1771369470,
|
||||
"narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "88d3861acdd3d2f0e361767018218e51810df8a1",
|
||||
"rev": "0182a361324364ae3f436a63005877674cf45efb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
inherit version;
|
||||
pname = "dms-shell";
|
||||
src = ./core;
|
||||
vendorHash = "sha256-cVUJXgzYMRSM0od1xzDVkMTdxHu3OIQX2bQ8AJbGQ1Q=";
|
||||
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ=";
|
||||
|
||||
subPackages = [ "cmd/dms" ];
|
||||
|
||||
@@ -181,7 +181,7 @@
|
||||
buildInputs =
|
||||
with pkgs;
|
||||
[
|
||||
go_1_24
|
||||
go_1_25
|
||||
gopls
|
||||
delve
|
||||
go-tools
|
||||
@@ -189,6 +189,7 @@
|
||||
|
||||
prek
|
||||
uv # for prek
|
||||
shellcheck
|
||||
|
||||
# Nix development tools
|
||||
nixd
|
||||
|
||||
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import QtCore
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -83,7 +84,12 @@ Singleton {
|
||||
if (desktopEntry && desktopEntry.icon) {
|
||||
return Quickshell.iconPath(desktopEntry.icon, true);
|
||||
}
|
||||
return Quickshell.iconPath(appId, true);
|
||||
|
||||
const icon = Quickshell.iconPath(appId, true);
|
||||
if (icon && icon !== "")
|
||||
return icon;
|
||||
|
||||
return DesktopService.resolveIconPath(appId);
|
||||
}
|
||||
|
||||
function getAppName(appId: string, desktopEntry: var): string {
|
||||
|
||||
@@ -142,6 +142,8 @@ Singleton {
|
||||
if (tabIndex !== undefined && popout.currentTabIndex !== undefined) {
|
||||
popout.currentTabIndex = tabIndex;
|
||||
}
|
||||
if (popout.updateSurfacePosition)
|
||||
popout.updateSurfacePosition();
|
||||
currentPopoutTriggers[screenName] = triggerId;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -126,6 +126,10 @@ Singleton {
|
||||
property var hiddenOutputDeviceNames: []
|
||||
property var hiddenInputDeviceNames: []
|
||||
|
||||
property string launcherLastMode: "all"
|
||||
property string appDrawerLastMode: "apps"
|
||||
property string niriOverviewLastMode: "apps"
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!isGreeterMode) {
|
||||
loadSettings();
|
||||
@@ -1100,6 +1104,21 @@ Singleton {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setLauncherLastMode(mode) {
|
||||
launcherLastMode = mode;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setAppDrawerLastMode(mode) {
|
||||
appDrawerLastMode = mode;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setNiriOverviewLastMode(mode) {
|
||||
niriOverviewLastMode = mode;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function syncWallpaperForCurrentMode() {
|
||||
if (!perModeWallpaper)
|
||||
return;
|
||||
|
||||
@@ -60,6 +60,7 @@ Singleton {
|
||||
property bool _hasLoaded: false
|
||||
property bool _isReadOnly: false
|
||||
property bool _hasUnsavedChanges: false
|
||||
property bool _selfWrite: false
|
||||
property var _loadedSettingsSnapshot: null
|
||||
property var pluginSettings: ({})
|
||||
property var builtInPluginSettings: ({})
|
||||
@@ -517,7 +518,7 @@ Singleton {
|
||||
property int osdPosition: SettingsData.Position.BottomCenter
|
||||
property bool osdVolumeEnabled: true
|
||||
property bool osdMediaVolumeEnabled: true
|
||||
property bool osdMediaPlaybackEnabled: true
|
||||
property bool osdMediaPlaybackEnabled: false
|
||||
property bool osdBrightnessEnabled: true
|
||||
property bool osdIdleInhibitorEnabled: true
|
||||
property bool osdMicMuteEnabled: true
|
||||
@@ -571,6 +572,10 @@ Singleton {
|
||||
"widgetTransparency": 1.0,
|
||||
"squareCorners": false,
|
||||
"noBackground": false,
|
||||
"maximizeWidgetIcons": false,
|
||||
"maximizeWidgetText": false,
|
||||
"removeWidgetPadding": false,
|
||||
"widgetPadding": 8,
|
||||
"gothCornersEnabled": false,
|
||||
"gothCornerRadiusOverride": false,
|
||||
"gothCornerRadiusValue": 12,
|
||||
@@ -583,6 +588,7 @@ Singleton {
|
||||
"widgetOutlineOpacity": 1.0,
|
||||
"widgetOutlineThickness": 1,
|
||||
"fontScale": 1.0,
|
||||
"iconScale": 1.0,
|
||||
"autoHide": false,
|
||||
"autoHideDelay": 250,
|
||||
"showOnWindowsOpen": false,
|
||||
@@ -1025,6 +1031,7 @@ Singleton {
|
||||
elif command -v dconf >/dev/null 2>&1; then
|
||||
dconf read /org/gnome/desktop/interface/icon-theme 2>/dev/null | sed "s/'//g"
|
||||
fi`;
|
||||
|
||||
Proc.runCommand("detectCosmicIconTheme", ["sh", "-c", detectScript], (output, exitCode) => {
|
||||
if (exitCode !== 0)
|
||||
return;
|
||||
@@ -1243,6 +1250,7 @@ Singleton {
|
||||
function saveSettings() {
|
||||
if (_loading || _parseError || !_hasLoaded)
|
||||
return;
|
||||
_selfWrite = true;
|
||||
settingsFile.setText(JSON.stringify(Store.toJson(root), null, 2));
|
||||
if (_isReadOnly)
|
||||
_checkSettingsWritable();
|
||||
@@ -2589,7 +2597,13 @@ Singleton {
|
||||
blockWrites: true
|
||||
atomicWrites: true
|
||||
watchChanges: true
|
||||
onFileChanged: settingsFileReloadDebounce.restart()
|
||||
onFileChanged: {
|
||||
if (_selfWrite) {
|
||||
_selfWrite = false;
|
||||
return;
|
||||
}
|
||||
settingsFileReloadDebounce.restart();
|
||||
}
|
||||
onLoaded: {
|
||||
if (isGreeterMode)
|
||||
return;
|
||||
|
||||
@@ -961,7 +961,6 @@ Singleton {
|
||||
}
|
||||
|
||||
if (!isGreeterMode) {
|
||||
// Skip with matugen because, our script runner will do it.
|
||||
if (!matugenAvailable) {
|
||||
PortalService.setLightMode(light);
|
||||
}
|
||||
@@ -1170,21 +1169,23 @@ Singleton {
|
||||
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5;
|
||||
}
|
||||
|
||||
function barIconSize(barThickness, offset, noBackground) {
|
||||
function barIconSize(barThickness, offset, maximizeIcon, iconScale) {
|
||||
const defaultOffset = offset !== undefined ? offset : -6;
|
||||
const size = (noBackground ?? false) ? iconSizeLarge : iconSize;
|
||||
const size = (maximizeIcon ?? false) ? iconSizeLarge : iconSize;
|
||||
const s = iconScale !== undefined ? iconScale : 1.0;
|
||||
|
||||
return Math.round((barThickness / 48) * (size + defaultOffset));
|
||||
return Math.round((barThickness / 48) * (size + defaultOffset) * s);
|
||||
}
|
||||
|
||||
function barTextSize(barThickness, fontScale) {
|
||||
function barTextSize(barThickness, fontScale, maximizeText) {
|
||||
const scale = barThickness / 48;
|
||||
const dankBarScale = fontScale !== undefined ? fontScale : 1.0;
|
||||
const maxScale = (maximizeText ?? false) ? 1.5 : 1.0;
|
||||
if (scale <= 0.75)
|
||||
return Math.round(fontSizeSmall * 0.9 * dankBarScale);
|
||||
return Math.round(fontSizeSmall * 0.9 * dankBarScale * maxScale);
|
||||
if (scale >= 1.25)
|
||||
return Math.round(fontSizeMedium * dankBarScale);
|
||||
return Math.round(fontSizeSmall * dankBarScale);
|
||||
return Math.round(fontSizeMedium * dankBarScale * maxScale);
|
||||
return Math.round(fontSizeSmall * dankBarScale * maxScale);
|
||||
}
|
||||
|
||||
function getBatteryIcon(level, isCharging, batteryAvailable) {
|
||||
|
||||
@@ -77,7 +77,11 @@ var SPEC = {
|
||||
|
||||
deviceMaxVolumes: { def: {} },
|
||||
hiddenOutputDeviceNames: { def: [] },
|
||||
hiddenInputDeviceNames: { def: [] }
|
||||
hiddenInputDeviceNames: { def: [] },
|
||||
|
||||
launcherLastMode: { def: "all" },
|
||||
appDrawerLastMode: { def: "apps" },
|
||||
niriOverviewLastMode: { def: "apps" }
|
||||
};
|
||||
|
||||
function getValidKeys() {
|
||||
|
||||
@@ -341,7 +341,7 @@ var SPEC = {
|
||||
osdPosition: { def: 5 },
|
||||
osdVolumeEnabled: { def: true },
|
||||
osdMediaVolumeEnabled: { def: true },
|
||||
osdMediaPlaybackEnabled: { def: true },
|
||||
osdMediaPlaybackEnabled: { def: false },
|
||||
osdBrightnessEnabled: { def: true },
|
||||
osdIdleInhibitorEnabled: { def: true },
|
||||
osdMicMuteEnabled: { def: true },
|
||||
@@ -395,6 +395,10 @@ var SPEC = {
|
||||
widgetTransparency: 1.0,
|
||||
squareCorners: false,
|
||||
noBackground: false,
|
||||
maximizeWidgetIcons: false,
|
||||
maximizeWidgetText: false,
|
||||
removeWidgetPadding: false,
|
||||
widgetPadding: 8,
|
||||
gothCornersEnabled: false,
|
||||
gothCornerRadiusOverride: false,
|
||||
gothCornerRadiusValue: 12,
|
||||
@@ -407,6 +411,7 @@ var SPEC = {
|
||||
widgetOutlineOpacity: 1.0,
|
||||
widgetOutlineThickness: 1,
|
||||
fontScale: 1.0,
|
||||
iconScale: 1.0,
|
||||
autoHide: false,
|
||||
autoHideDelay: 250,
|
||||
showOnWindowsOpen: false,
|
||||
|
||||
@@ -142,25 +142,45 @@ Item {
|
||||
fadeDpmsWindowLoader.item.cancelFade();
|
||||
}
|
||||
}
|
||||
|
||||
function onRequestMonitorOn() {
|
||||
if (!fadeDpmsWindowLoader.item)
|
||||
return;
|
||||
fadeDpmsWindowLoader.item.cancelFade();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property string _barLayoutStateJson: {
|
||||
const configs = SettingsData.barConfigs;
|
||||
const mapped = configs.map(c => ({
|
||||
id: c.id,
|
||||
position: c.position,
|
||||
autoHide: c.autoHide,
|
||||
visible: c.visible
|
||||
})).sort((a, b) => {
|
||||
const aVertical = a.position === SettingsData.Position.Left || a.position === SettingsData.Position.Right;
|
||||
const bVertical = b.position === SettingsData.Position.Left || b.position === SettingsData.Position.Right;
|
||||
if (aVertical !== bVertical) {
|
||||
return aVertical - bVertical;
|
||||
}
|
||||
return String(a.id).localeCompare(String(b.id));
|
||||
});
|
||||
return JSON.stringify(mapped);
|
||||
}
|
||||
|
||||
on_BarLayoutStateJsonChanged: {
|
||||
if (typeof dockRecreateDebounce !== "undefined") {
|
||||
dockRecreateDebounce.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: dankBarRepeater
|
||||
model: ScriptModel {
|
||||
id: barRepeaterModel
|
||||
values: {
|
||||
const configs = SettingsData.barConfigs;
|
||||
return configs.map(c => ({
|
||||
id: c.id,
|
||||
position: c.position
|
||||
})).sort((a, b) => {
|
||||
const aVertical = a.position === SettingsData.Position.Left || a.position === SettingsData.Position.Right;
|
||||
const bVertical = b.position === SettingsData.Position.Left || b.position === SettingsData.Position.Right;
|
||||
return aVertical - bVertical;
|
||||
});
|
||||
}
|
||||
values: JSON.parse(root._barLayoutStateJson)
|
||||
}
|
||||
|
||||
property var hyprlandOverviewLoaderRef: hyprlandOverviewLoader
|
||||
@@ -207,13 +227,6 @@ Item {
|
||||
PolkitService.polkitAvailable;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onBarConfigsChanged() {
|
||||
dockRecreateDebounce.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: dockLoader
|
||||
active: root.dockEnabled
|
||||
@@ -265,6 +278,7 @@ Item {
|
||||
sourceComponent: Component {
|
||||
DankDashPopout {
|
||||
id: dankDashPopout
|
||||
onPopoutClosed: PopoutService.unloadDankDash()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -284,8 +298,13 @@ Item {
|
||||
|
||||
active: false
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.notificationCenterLoader = notificationCenterLoader;
|
||||
}
|
||||
|
||||
NotificationCenterPopout {
|
||||
id: notificationCenter
|
||||
onPopoutClosed: PopoutService.unloadNotificationCenter()
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.notificationCenterPopout = notificationCenter;
|
||||
@@ -309,10 +328,15 @@ Item {
|
||||
property var modalRef: colorPickerModal
|
||||
property LazyLoader powerModalLoaderRef: powerMenuModalLoader
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.controlCenterLoader = controlCenterLoader;
|
||||
}
|
||||
|
||||
ControlCenterPopout {
|
||||
id: controlCenterPopout
|
||||
colorPickerModal: controlCenterLoader.modalRef
|
||||
powerMenuModalLoader: controlCenterLoader.powerModalLoaderRef
|
||||
onPopoutClosed: PopoutService.unloadControlCenter()
|
||||
|
||||
onLockRequested: {
|
||||
lock.activate();
|
||||
@@ -420,8 +444,13 @@ Item {
|
||||
|
||||
active: false
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.batteryPopoutLoader = batteryPopoutLoader;
|
||||
}
|
||||
|
||||
BatteryPopout {
|
||||
id: batteryPopout
|
||||
onPopoutClosed: PopoutService.unloadBattery()
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.batteryPopout = batteryPopout;
|
||||
@@ -434,8 +463,13 @@ Item {
|
||||
|
||||
active: false
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.layoutPopoutLoader = layoutPopoutLoader;
|
||||
}
|
||||
|
||||
DWLLayoutPopout {
|
||||
id: layoutPopout
|
||||
onPopoutClosed: PopoutService.unloadLayoutPopout()
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.layoutPopout = layoutPopout;
|
||||
@@ -448,8 +482,13 @@ Item {
|
||||
|
||||
active: false
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.vpnPopoutLoader = vpnPopoutLoader;
|
||||
}
|
||||
|
||||
VpnPopout {
|
||||
id: vpnPopout
|
||||
onPopoutClosed: PopoutService.unloadVpn()
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.vpnPopout = vpnPopout;
|
||||
@@ -462,8 +501,13 @@ Item {
|
||||
|
||||
active: false
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.processListPopoutLoader = processListPopoutLoader;
|
||||
}
|
||||
|
||||
ProcessListPopout {
|
||||
id: processListPopout
|
||||
onPopoutClosed: PopoutService.unloadProcessListPopout()
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.processListPopout = processListPopout;
|
||||
@@ -506,8 +550,13 @@ Item {
|
||||
|
||||
active: false
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.appDrawerLoader = appDrawerLoader;
|
||||
}
|
||||
|
||||
AppDrawerPopout {
|
||||
id: appDrawerPopout
|
||||
onPopoutClosed: PopoutService.unloadAppDrawer()
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.appDrawerPopout = appDrawerPopout;
|
||||
@@ -539,8 +588,13 @@ Item {
|
||||
|
||||
active: false
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.clipboardHistoryPopoutLoader = clipboardHistoryPopoutLoader;
|
||||
}
|
||||
|
||||
ClipboardHistoryPopout {
|
||||
id: clipboardHistoryPopout
|
||||
onPopoutClosed: PopoutService.unloadClipboardHistoryPopout()
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.clipboardHistoryPopout = clipboardHistoryPopout;
|
||||
@@ -715,8 +769,13 @@ Item {
|
||||
|
||||
active: false
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.systemUpdateLoader = systemUpdateLoader;
|
||||
}
|
||||
|
||||
SystemUpdatePopout {
|
||||
id: systemUpdatePopout
|
||||
onPopoutClosed: PopoutService.unloadSystemUpdate()
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.systemUpdatePopout = systemUpdatePopout;
|
||||
|
||||
@@ -859,6 +859,70 @@ Item {
|
||||
return success ? `WIDGET_TOGGLE_SUCCESS: ${widgetId}` : `WIDGET_TOGGLE_FAILED: ${widgetId}`;
|
||||
}
|
||||
|
||||
function openWith(widgetId: string, mode: string): string {
|
||||
if (!widgetId)
|
||||
return "ERROR: No widget ID specified";
|
||||
if (!BarWidgetService.hasWidget(widgetId))
|
||||
return `WIDGET_NOT_FOUND: ${widgetId}`;
|
||||
|
||||
const widget = BarWidgetService.getWidgetOnFocusedScreen(widgetId);
|
||||
if (!widget)
|
||||
return `WIDGET_NOT_AVAILABLE: ${widgetId}`;
|
||||
if (typeof widget.openWithMode !== "function")
|
||||
return `WIDGET_OPEN_WITH_NOT_SUPPORTED: ${widgetId}`;
|
||||
|
||||
widget.openWithMode(mode || "all");
|
||||
return `WIDGET_OPEN_WITH_SUCCESS: ${widgetId} ${mode}`;
|
||||
}
|
||||
|
||||
function toggleWith(widgetId: string, mode: string): string {
|
||||
if (!widgetId)
|
||||
return "ERROR: No widget ID specified";
|
||||
if (!BarWidgetService.hasWidget(widgetId))
|
||||
return `WIDGET_NOT_FOUND: ${widgetId}`;
|
||||
|
||||
const widget = BarWidgetService.getWidgetOnFocusedScreen(widgetId);
|
||||
if (!widget)
|
||||
return `WIDGET_NOT_AVAILABLE: ${widgetId}`;
|
||||
if (typeof widget.toggleWithMode !== "function")
|
||||
return `WIDGET_TOGGLE_WITH_NOT_SUPPORTED: ${widgetId}`;
|
||||
|
||||
widget.toggleWithMode(mode || "all");
|
||||
return `WIDGET_TOGGLE_WITH_SUCCESS: ${widgetId} ${mode}`;
|
||||
}
|
||||
|
||||
function openQuery(widgetId: string, query: string): string {
|
||||
if (!widgetId)
|
||||
return "ERROR: No widget ID specified";
|
||||
if (!BarWidgetService.hasWidget(widgetId))
|
||||
return `WIDGET_NOT_FOUND: ${widgetId}`;
|
||||
|
||||
const widget = BarWidgetService.getWidgetOnFocusedScreen(widgetId);
|
||||
if (!widget)
|
||||
return `WIDGET_NOT_AVAILABLE: ${widgetId}`;
|
||||
if (typeof widget.openWithQuery !== "function")
|
||||
return `WIDGET_OPEN_QUERY_NOT_SUPPORTED: ${widgetId}`;
|
||||
|
||||
widget.openWithQuery(query || "");
|
||||
return `WIDGET_OPEN_QUERY_SUCCESS: ${widgetId}`;
|
||||
}
|
||||
|
||||
function toggleQuery(widgetId: string, query: string): string {
|
||||
if (!widgetId)
|
||||
return "ERROR: No widget ID specified";
|
||||
if (!BarWidgetService.hasWidget(widgetId))
|
||||
return `WIDGET_NOT_FOUND: ${widgetId}`;
|
||||
|
||||
const widget = BarWidgetService.getWidgetOnFocusedScreen(widgetId);
|
||||
if (!widget)
|
||||
return `WIDGET_NOT_AVAILABLE: ${widgetId}`;
|
||||
if (typeof widget.toggleWithQuery !== "function")
|
||||
return `WIDGET_TOGGLE_QUERY_NOT_SUPPORTED: ${widgetId}`;
|
||||
|
||||
widget.toggleWithQuery(query || "");
|
||||
return `WIDGET_TOGGLE_QUERY_SUCCESS: ${widgetId}`;
|
||||
}
|
||||
|
||||
function list(): string {
|
||||
const widgets = BarWidgetService.getRegisteredWidgetIds();
|
||||
if (widgets.length === 0)
|
||||
|
||||
@@ -18,7 +18,7 @@ FloatingWindow {
|
||||
}
|
||||
|
||||
objectName: "changelogModal"
|
||||
title: "What's New"
|
||||
title: i18n("What's New")
|
||||
minimumSize: Qt.size(modalWidth, modalHeight)
|
||||
maximumSize: Qt.size(modalWidth, modalHeight)
|
||||
color: Theme.surfaceContainer
|
||||
@@ -81,7 +81,7 @@ FloatingWindow {
|
||||
onClicked: root.dismiss()
|
||||
|
||||
DankTooltip {
|
||||
text: "Close"
|
||||
text: i18n("Close")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,7 +125,7 @@ FloatingWindow {
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankButton {
|
||||
text: "Read Full Release Notes"
|
||||
text: i18n("Read Full Release Notes")
|
||||
iconName: "open_in_new"
|
||||
backgroundColor: Theme.surfaceContainerHighest
|
||||
textColor: Theme.surfaceText
|
||||
@@ -133,7 +133,7 @@ FloatingWindow {
|
||||
}
|
||||
|
||||
DankButton {
|
||||
text: "Got It"
|
||||
text: i18n("Got It")
|
||||
iconName: "check"
|
||||
backgroundColor: Theme.primary
|
||||
textColor: Theme.primaryText
|
||||
|
||||
@@ -12,7 +12,7 @@ Singleton {
|
||||
readonly property int popoutWidth: 550
|
||||
readonly property int popoutHeight: 500
|
||||
readonly property int itemHeight: 72
|
||||
readonly property int thumbnailSize: 48
|
||||
readonly property int thumbnailSize: 100
|
||||
readonly property int retryInterval: 50
|
||||
readonly property int viewportBuffer: 100
|
||||
readonly property int extendedBuffer: 200
|
||||
|
||||
@@ -84,7 +84,8 @@ Rectangle {
|
||||
anchors.right: actionButtons.left
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: contentColumn.implicitHeight
|
||||
// height: contentColumn.implicitHeight
|
||||
height: ClipboardConstants.itemHeight
|
||||
clip: true
|
||||
|
||||
ClipboardThumbnail {
|
||||
@@ -92,7 +93,7 @@ Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize
|
||||
height: entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize
|
||||
height: entryType === "image" ? ClipboardConstants.itemHeight - 4 : Theme.iconSize // 100 - 4 = 96, 96:72 = 4:3
|
||||
entry: root.entry
|
||||
entryType: root.entryType
|
||||
modal: root.modal
|
||||
@@ -134,6 +135,7 @@ Rectangle {
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: entryType === "long_text" ? 3 : 1
|
||||
elide: Text.ElideRight
|
||||
textFormat: Text.PlainText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,23 +137,23 @@ Item {
|
||||
anchors.margins: 2
|
||||
source: thumbnailImage
|
||||
maskEnabled: true
|
||||
maskSource: clipboardCircularMask
|
||||
maskSource: clipboardRoundedRectangularMask
|
||||
visible: entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != ""
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
|
||||
Item {
|
||||
id: clipboardCircularMask
|
||||
width: ClipboardConstants.thumbnailSize - 4
|
||||
height: ClipboardConstants.thumbnailSize - 4
|
||||
id: clipboardRoundedRectangularMask
|
||||
width: ClipboardConstants.thumbnailSize
|
||||
height: ClipboardConstants.itemHeight - 4
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
radius: Theme.cornerRadius / 2 // Thumbnail corner radius is divided by 2 so it doesnt look weird on large corner radius (eg: 32px)
|
||||
color: "black"
|
||||
antialiasing: true
|
||||
}
|
||||
|
||||
@@ -56,11 +56,6 @@ Rectangle {
|
||||
case "app":
|
||||
if (selectedItem?.isCore)
|
||||
break;
|
||||
if (selectedItem?.actions) {
|
||||
for (var i = 0; i < selectedItem.actions.length; i++) {
|
||||
result.push(selectedItem.actions[i]);
|
||||
}
|
||||
}
|
||||
if (SessionService.nvidiaCommand) {
|
||||
result.push({
|
||||
name: I18n.tr("Launch on dGPU"),
|
||||
@@ -68,6 +63,11 @@ Rectangle {
|
||||
action: "launch_dgpu"
|
||||
});
|
||||
}
|
||||
if (selectedItem?.actions) {
|
||||
for (var i = 0; i < selectedItem.actions.length; i++) {
|
||||
result.push(selectedItem.actions[i]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -125,13 +125,6 @@ Item {
|
||||
}
|
||||
|
||||
readonly property var sectionDefinitions: [
|
||||
{
|
||||
id: "calculator",
|
||||
title: I18n.tr("Calculator"),
|
||||
icon: "calculate",
|
||||
priority: 0,
|
||||
defaultViewMode: "list"
|
||||
},
|
||||
{
|
||||
id: "favorites",
|
||||
title: I18n.tr("Pinned"),
|
||||
@@ -681,24 +674,16 @@ Item {
|
||||
return;
|
||||
}
|
||||
|
||||
var calculatorResult = evaluateCalculator(searchQuery);
|
||||
if (calculatorResult) {
|
||||
calculatorResult._preScored = 12000;
|
||||
allItems.push(calculatorResult);
|
||||
}
|
||||
|
||||
var apps = searchApps(searchQuery);
|
||||
for (var i = 0; i < apps.length; i++) {
|
||||
if (searchQuery)
|
||||
apps[i]._preScored = 11000 - i;
|
||||
allItems.push(apps[i]);
|
||||
}
|
||||
|
||||
if (searchMode === "all") {
|
||||
if (searchQuery && searchQuery.length >= 2) {
|
||||
_pluginPhasePending = true;
|
||||
_phase1Items = allItems.slice();
|
||||
_pluginPhaseForceFirst = shouldResetSelection;
|
||||
_phase1Items = allItems;
|
||||
pluginPhaseTimer.restart();
|
||||
isSearching = true;
|
||||
searchCompleted();
|
||||
@@ -931,13 +916,6 @@ Item {
|
||||
return Transform.transformFileResult(file, I18n.tr("Open"), I18n.tr("Open folder"), I18n.tr("Copy path"));
|
||||
}
|
||||
|
||||
function evaluateCalculator(query) {
|
||||
var calc = Utils.evaluateCalculator(query);
|
||||
if (!calc)
|
||||
return null;
|
||||
return Transform.createCalculatorItem(calc, query, I18n.tr("Copy"));
|
||||
}
|
||||
|
||||
function detectTrigger(query) {
|
||||
if (!query || query.length === 0)
|
||||
return {
|
||||
@@ -1270,6 +1248,23 @@ Item {
|
||||
CacheData.saveLauncherCache(serializable);
|
||||
}
|
||||
|
||||
function _actionsFromDesktopEntry(appId) {
|
||||
if (!appId)
|
||||
return [];
|
||||
var entry = DesktopEntries.heuristicLookup(appId);
|
||||
if (!entry || !entry.actions || entry.actions.length === 0)
|
||||
return [];
|
||||
var result = [];
|
||||
for (var i = 0; i < entry.actions.length; i++) {
|
||||
result.push({
|
||||
name: entry.actions[i].name,
|
||||
icon: "play_arrow",
|
||||
actionData: entry.actions[i]
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function _loadDiskCache() {
|
||||
var cached = CacheData.loadLauncherCache();
|
||||
if (!cached || !Array.isArray(cached) || cached.length === 0)
|
||||
@@ -1297,8 +1292,12 @@ Item {
|
||||
data: {
|
||||
id: it.id
|
||||
},
|
||||
actions: [],
|
||||
primaryAction: null,
|
||||
actions: _actionsFromDesktopEntry(it.id),
|
||||
primaryAction: it.type === "app" && !it.isCore ? {
|
||||
name: I18n.tr("Launch"),
|
||||
icon: "open_in_new",
|
||||
action: "launch"
|
||||
} : null,
|
||||
_diskCached: true,
|
||||
_hName: "",
|
||||
_hSub: "",
|
||||
@@ -1559,9 +1558,6 @@ Item {
|
||||
case "file":
|
||||
openFile(item.data?.path);
|
||||
break;
|
||||
case "calculator":
|
||||
copyToClipboard(item.name);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
@@ -1616,25 +1612,41 @@ Item {
|
||||
itemExecuted();
|
||||
}
|
||||
|
||||
function launchApp(app) {
|
||||
function _resolveDesktopEntry(app) {
|
||||
if (!app)
|
||||
return null;
|
||||
if (app.command)
|
||||
return app;
|
||||
var id = app.id || app.execString || app.exec || "";
|
||||
if (!id)
|
||||
return null;
|
||||
return DesktopEntries.heuristicLookup(id);
|
||||
}
|
||||
|
||||
function launchApp(app) {
|
||||
var entry = _resolveDesktopEntry(app);
|
||||
if (!entry)
|
||||
return;
|
||||
SessionService.launchDesktopEntry(app);
|
||||
AppUsageHistoryData.addAppUsage(app);
|
||||
SessionService.launchDesktopEntry(entry);
|
||||
AppUsageHistoryData.addAppUsage(entry);
|
||||
}
|
||||
|
||||
function launchAppWithNvidia(app) {
|
||||
if (!app)
|
||||
var entry = _resolveDesktopEntry(app);
|
||||
if (!entry)
|
||||
return;
|
||||
SessionService.launchDesktopEntry(app, true);
|
||||
AppUsageHistoryData.addAppUsage(app);
|
||||
SessionService.launchDesktopEntry(entry, true);
|
||||
AppUsageHistoryData.addAppUsage(entry);
|
||||
}
|
||||
|
||||
function launchAppAction(actionItem) {
|
||||
if (!actionItem || !actionItem.parentApp || !actionItem.actionData)
|
||||
if (!actionItem || !actionItem.actionData)
|
||||
return;
|
||||
SessionService.launchDesktopAction(actionItem.parentApp, actionItem.actionData);
|
||||
AppUsageHistoryData.addAppUsage(actionItem.parentApp);
|
||||
var entry = _resolveDesktopEntry(actionItem.parentApp);
|
||||
if (!entry)
|
||||
return;
|
||||
SessionService.launchDesktopAction(entry, actionItem.actionData);
|
||||
AppUsageHistoryData.addAppUsage(entry);
|
||||
}
|
||||
|
||||
function openFile(path) {
|
||||
|
||||
@@ -101,35 +101,6 @@ function detectIconType(iconName) {
|
||||
return "material";
|
||||
}
|
||||
|
||||
function evaluateCalculator(query) {
|
||||
if (!query || query.length === 0)
|
||||
return null;
|
||||
|
||||
var mathExpr = query.replace(/[^0-9+\-*/().%\s^]/g, "");
|
||||
if (mathExpr.length < 2)
|
||||
return null;
|
||||
|
||||
var hasMath = /[+\-*/^%]/.test(query) && /\d/.test(query);
|
||||
if (!hasMath)
|
||||
return null;
|
||||
|
||||
try {
|
||||
var sanitized = mathExpr.replace(/\^/g, "**");
|
||||
var result = Function('"use strict"; return (' + sanitized + ')')();
|
||||
|
||||
if (typeof result === "number" && isFinite(result)) {
|
||||
var displayResult = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/\.?0+$/, "");
|
||||
return {
|
||||
expression: query,
|
||||
result: result,
|
||||
displayResult: displayResult
|
||||
};
|
||||
}
|
||||
} catch (e) { }
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function sortPluginIdsByOrder(pluginIds, order) {
|
||||
if (!order || order.length === 0)
|
||||
return pluginIds;
|
||||
|
||||
@@ -102,7 +102,7 @@ Item {
|
||||
spotlightContent.searchField.text = query;
|
||||
}
|
||||
if (spotlightContent.controller) {
|
||||
var targetMode = mode || "all";
|
||||
var targetMode = mode || SessionData.launcherLastMode || "all";
|
||||
spotlightContent.controller.searchMode = targetMode;
|
||||
spotlightContent.controller.activePluginId = "";
|
||||
spotlightContent.controller.activePluginName = "";
|
||||
@@ -226,6 +226,15 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: spotlightContent?.controller ?? null
|
||||
function onModeChanged(mode) {
|
||||
if (spotlightContent.controller.autoSwitchedToFiles)
|
||||
return;
|
||||
SessionData.setLauncherLastMode(mode);
|
||||
}
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: focusGrab
|
||||
windows: [launcherWindow]
|
||||
@@ -284,7 +293,7 @@ Item {
|
||||
|
||||
PanelWindow {
|
||||
id: launcherWindow
|
||||
visible: root._windowEnabled && (!root.unloadContentOnClose || spotlightOpen || isClosing)
|
||||
visible: root._windowEnabled && (spotlightOpen || isClosing)
|
||||
color: "transparent"
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
|
||||
@@ -190,32 +190,6 @@ function transformPluginItem(item, pluginId, selectLabel) {
|
||||
};
|
||||
}
|
||||
|
||||
function createCalculatorItem(calc, query, copyLabel) {
|
||||
return {
|
||||
id: "calculator_result",
|
||||
type: "calculator",
|
||||
name: calc.displayResult,
|
||||
subtitle: query + " =",
|
||||
icon: "calculate",
|
||||
iconType: "material",
|
||||
section: "calculator",
|
||||
data: {
|
||||
expression: calc.expression,
|
||||
result: calc.result
|
||||
},
|
||||
actions: [],
|
||||
primaryAction: {
|
||||
name: copyLabel,
|
||||
icon: "content_copy",
|
||||
action: "copy"
|
||||
},
|
||||
_hName: "",
|
||||
_hSub: "",
|
||||
_hRich: false,
|
||||
_preScored: undefined
|
||||
};
|
||||
}
|
||||
|
||||
function createPluginBrowseItem(pluginId, plugin, trigger, isBuiltIn, isAllowed, browseLabel, triggerLabel, noTriggerLabel) {
|
||||
var rawIcon = isBuiltIn ? (plugin.cornerIcon || "extension") : (plugin.icon || "extension");
|
||||
return {
|
||||
|
||||
@@ -470,9 +470,6 @@ FocusScope {
|
||||
|
||||
onTextChanged: {
|
||||
controller.setSearchQuery(text);
|
||||
if (text.length === 0) {
|
||||
controller.restorePreviousMode();
|
||||
}
|
||||
if (actionPanel.expanded) {
|
||||
actionPanel.hide();
|
||||
}
|
||||
|
||||
@@ -81,6 +81,12 @@ function calculateNextIndex(flatModel, selectedFlatIndex, sectionId, viewMode, g
|
||||
return bounds.start + newPosInSection;
|
||||
}
|
||||
|
||||
var currentRow = Math.floor(posInSection / cols);
|
||||
var lastRow = Math.floor((bounds.count - 1) / cols);
|
||||
if (currentRow < lastRow) {
|
||||
return bounds.start + bounds.count - 1;
|
||||
}
|
||||
|
||||
var nextSection = findNextNonHeaderIndex(flatModel, bounds.end + 1);
|
||||
return nextSection !== -1 ? nextSection : selectedFlatIndex;
|
||||
}
|
||||
|
||||
@@ -178,8 +178,6 @@ Rectangle {
|
||||
if (!root.item)
|
||||
return "";
|
||||
switch (root.item.type) {
|
||||
case "calculator":
|
||||
return I18n.tr("Calc");
|
||||
case "plugin":
|
||||
return I18n.tr("Plugin");
|
||||
case "file":
|
||||
|
||||
@@ -130,24 +130,17 @@ Item {
|
||||
if (!entry || entry.isHeader)
|
||||
return;
|
||||
var rowIndex = _flatIndexToRowMap[index];
|
||||
if (rowIndex === undefined || rowIndex >= _cumulativeHeights.length)
|
||||
return;
|
||||
var row = _visualRows[rowIndex];
|
||||
if (!row)
|
||||
if (rowIndex === undefined)
|
||||
return;
|
||||
|
||||
var rowY = _cumulativeHeights[rowIndex];
|
||||
var rowHeight = row.height;
|
||||
var scrollY = mainListView.contentY - mainListView.originY;
|
||||
var viewHeight = mainListView.height;
|
||||
var headerH = stickyHeader.height;
|
||||
mainListView.positionViewAtIndex(rowIndex, ListView.Contain);
|
||||
|
||||
if (rowY < scrollY + headerH) {
|
||||
mainListView.contentY = Math.max(mainListView.originY, rowY - headerH + mainListView.originY);
|
||||
return;
|
||||
}
|
||||
if (rowY + rowHeight > scrollY + viewHeight) {
|
||||
mainListView.contentY = rowY + rowHeight - viewHeight + mainListView.originY;
|
||||
if (stickyHeader.visible && rowIndex < _cumulativeHeights.length) {
|
||||
var rowY = _cumulativeHeights[rowIndex];
|
||||
var scrollY = mainListView.contentY - mainListView.originY;
|
||||
if (rowY < scrollY + stickyHeader.height) {
|
||||
mainListView.contentY = Math.max(mainListView.originY, rowY - stickyHeader.height + mainListView.originY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
const Weights = {
|
||||
exactMatch: 10000,
|
||||
prefixMatch: 5000,
|
||||
wordBoundary: 1000,
|
||||
wordBoundary: 3000,
|
||||
substring: 500,
|
||||
fuzzy: 100,
|
||||
frecency: 2000,
|
||||
@@ -99,8 +99,8 @@ function getTimeBucketWeight(daysSinceUsed) {
|
||||
function calculateTextScore(name, query) {
|
||||
if (name === query) return Weights.exactMatch
|
||||
if (name.startsWith(query)) return Weights.prefixMatch
|
||||
if (name.includes(query)) return Weights.substring
|
||||
if (hasWordBoundaryMatch(name, query)) return Weights.wordBoundary
|
||||
if (name.includes(query)) return Weights.substring
|
||||
|
||||
if (query.length >= 3) {
|
||||
var fs = fuzzyScore(name, query)
|
||||
@@ -140,7 +140,7 @@ function score(item, query, frecencyData) {
|
||||
|
||||
if (textScore === 0) return 0
|
||||
|
||||
var usageBonus = frecencyData ? Math.min(frecencyData.usageCount * 10, Weights.frecency) : 0
|
||||
var usageBonus = frecencyData ? Math.min(frecencyData.usageCount * 50, Weights.frecency) : 0
|
||||
|
||||
return textScore + usageBonus + typeBonus
|
||||
}
|
||||
|
||||
@@ -238,37 +238,37 @@ FocusScope {
|
||||
|
||||
property var quickAccessLocations: [
|
||||
{
|
||||
"name": "Home",
|
||||
"name": I18n.tr("Home"),
|
||||
"path": homeDir,
|
||||
"icon": "home"
|
||||
},
|
||||
{
|
||||
"name": "Documents",
|
||||
"name": I18n.tr("Documents"),
|
||||
"path": docsDir,
|
||||
"icon": "description"
|
||||
},
|
||||
{
|
||||
"name": "Downloads",
|
||||
"name": I18n.tr("Downloads"),
|
||||
"path": downloadDir,
|
||||
"icon": "download"
|
||||
},
|
||||
{
|
||||
"name": "Pictures",
|
||||
"name": I18n.tr("Pictures"),
|
||||
"path": picsDir,
|
||||
"icon": "image"
|
||||
},
|
||||
{
|
||||
"name": "Music",
|
||||
"name": I18n.tr("Music"),
|
||||
"path": musicDir,
|
||||
"icon": "music_note"
|
||||
},
|
||||
{
|
||||
"name": "Videos",
|
||||
"name": I18n.tr("Videos"),
|
||||
"path": videosDir,
|
||||
"icon": "movie"
|
||||
},
|
||||
{
|
||||
"name": "Desktop",
|
||||
"name": I18n.tr("Desktop"),
|
||||
"path": desktopDir,
|
||||
"icon": "computer"
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ StyledRect {
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: "Quick Access"
|
||||
text: I18n.tr("Quick Access")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
font.weight: Font.Medium
|
||||
|
||||
@@ -91,7 +91,12 @@ DankModal {
|
||||
id: searchField
|
||||
Layout.alignment: Qt.AlignRight
|
||||
leftIconName: "search"
|
||||
keyForwardTargets: [root.modalFocusScope]
|
||||
onTextEdited: searchDebounce.restart()
|
||||
Keys.onEscapePressed: event => {
|
||||
root.close();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +123,7 @@ DankModal {
|
||||
|
||||
function generateCategories(query) {
|
||||
const lowerQuery = query ? query.toLowerCase().trim() : "";
|
||||
const lowerQueryWords = query.split(/\s+/);
|
||||
const processed = {};
|
||||
|
||||
for (const cat in rawBinds) {
|
||||
@@ -130,10 +136,25 @@ DankModal {
|
||||
const keyLower = bind.key.toLowerCase();
|
||||
const descLower = bind.desc.toLowerCase();
|
||||
const actionLower = bind.action.toLowerCase();
|
||||
if (!(lowerQuery.length === 0 || keyLower.includes(lowerQuery) || descLower.includes(lowerQuery) || catLower.includes(lowerQuery) || actionLower.includes(lowerQuery)))
|
||||
continue;
|
||||
|
||||
if (bind.hideOnOverlay)
|
||||
continue;
|
||||
let shouldContinue = false;
|
||||
for (let j = 0; j < lowerQueryWords.length; j++) {
|
||||
const word = lowerQueryWords[j];
|
||||
if (!(
|
||||
word.length === 0 ||
|
||||
keyLower.includes(word) ||
|
||||
descLower.includes(word) ||
|
||||
catLower.includes(word) ||
|
||||
actionLower.includes(word)
|
||||
)) {
|
||||
shouldContinue = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (shouldContinue)
|
||||
continue;
|
||||
|
||||
if (bind.subcat) {
|
||||
hasSubcats = true;
|
||||
|
||||
@@ -83,9 +83,9 @@ FloatingWindow {
|
||||
|
||||
objectName: "processListModal"
|
||||
title: I18n.tr("System Monitor", "sysmon window title")
|
||||
minimumSize: Qt.size(750, 550)
|
||||
implicitWidth: 1000
|
||||
implicitHeight: 720
|
||||
minimumSize: Qt.size(Math.min(Math.round(Theme.fontSizeMedium * 48), Screen.width), Math.min(Math.round(Theme.fontSizeMedium * 34), Screen.height))
|
||||
implicitWidth: Math.round(Theme.fontSizeMedium * 71)
|
||||
implicitHeight: Math.round(Theme.fontSizeMedium * 51)
|
||||
color: Theme.surfaceContainer
|
||||
visible: false
|
||||
|
||||
@@ -236,7 +236,7 @@ FloatingWindow {
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 48
|
||||
Layout.preferredHeight: Math.round(Theme.fontSizeMedium * 3.4)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
@@ -293,10 +293,10 @@ FloatingWindow {
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 52
|
||||
Layout.preferredHeight: Math.round(Theme.fontSizeMedium * 3.7)
|
||||
Layout.leftMargin: Theme.spacingL
|
||||
Layout.rightMargin: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
spacing: 2
|
||||
@@ -322,14 +322,15 @@ FloatingWindow {
|
||||
]
|
||||
|
||||
Rectangle {
|
||||
width: 120
|
||||
height: 44
|
||||
width: tabRowContent.implicitWidth + Theme.spacingM * 2
|
||||
height: Math.round(Theme.fontSizeMedium * 3.1)
|
||||
radius: Theme.cornerRadius
|
||||
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
|
||||
border.color: currentTab === index ? Theme.primary : "transparent"
|
||||
border.width: currentTab === index ? 1 : 0
|
||||
|
||||
Row {
|
||||
id: tabRowContent
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
@@ -373,11 +374,13 @@ FloatingWindow {
|
||||
|
||||
DankButtonGroup {
|
||||
id: processFilterGroup
|
||||
Layout.minimumWidth: implicitWidth + 8
|
||||
model: [I18n.tr("All"), I18n.tr("User"), I18n.tr("System")]
|
||||
currentIndex: 0
|
||||
checkEnabled: false
|
||||
buttonHeight: 36
|
||||
buttonHeight: Math.round(Theme.fontSizeSmall * 2.6)
|
||||
minButtonWidth: 0
|
||||
buttonPadding: Theme.spacingS
|
||||
textSize: Theme.fontSizeSmall
|
||||
visible: currentTab === 0
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (!selected)
|
||||
@@ -400,9 +403,9 @@ FloatingWindow {
|
||||
DankTextField {
|
||||
id: searchField
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: 250
|
||||
Layout.minimumWidth: 120
|
||||
Layout.preferredHeight: 40
|
||||
Layout.maximumWidth: Math.round(Theme.fontSizeMedium * 18)
|
||||
Layout.minimumWidth: Theme.fontSizeMedium * 4
|
||||
Layout.preferredHeight: Math.round(Theme.fontSizeMedium * 2.8)
|
||||
placeholderText: I18n.tr("Search processes...", "process search placeholder")
|
||||
leftIconName: "search"
|
||||
showClearButton: true
|
||||
@@ -470,7 +473,7 @@ FloatingWindow {
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 32
|
||||
Layout.preferredHeight: Math.round(Theme.fontSizeSmall * 2.7)
|
||||
Layout.leftMargin: Theme.spacingL
|
||||
Layout.rightMargin: Theme.spacingL
|
||||
Layout.bottomMargin: Theme.spacingM
|
||||
|
||||
@@ -631,7 +631,7 @@ FloatingWindow {
|
||||
anchors.fill: parent
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
textColor: Theme.surfaceText
|
||||
placeholderText: "fixed 800"
|
||||
placeholderText: "800"
|
||||
backgroundColor: "transparent"
|
||||
enabled: root.visible
|
||||
}
|
||||
@@ -658,7 +658,7 @@ FloatingWindow {
|
||||
anchors.fill: parent
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
textColor: Theme.surfaceText
|
||||
placeholderText: "fixed 600"
|
||||
placeholderText: "600"
|
||||
backgroundColor: "transparent"
|
||||
enabled: root.visible
|
||||
}
|
||||
|
||||
@@ -8,10 +8,39 @@ DankPopout {
|
||||
|
||||
layerNamespace: "dms:app-launcher"
|
||||
|
||||
property string _pendingMode: ""
|
||||
property string _pendingQuery: ""
|
||||
|
||||
function show() {
|
||||
open();
|
||||
}
|
||||
|
||||
function openWithMode(mode) {
|
||||
_pendingMode = mode || "";
|
||||
open();
|
||||
}
|
||||
|
||||
function toggleWithMode(mode) {
|
||||
if (shouldBeVisible) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
openWithMode(mode);
|
||||
}
|
||||
|
||||
function openWithQuery(query) {
|
||||
_pendingQuery = query || "";
|
||||
open();
|
||||
}
|
||||
|
||||
function toggleWithQuery(query) {
|
||||
if (shouldBeVisible) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
openWithQuery(query);
|
||||
}
|
||||
|
||||
popupWidth: 560
|
||||
popupHeight: 640
|
||||
triggerWidth: 40
|
||||
@@ -30,20 +59,39 @@ DankPopout {
|
||||
var lc = contentLoader.item?.launcherContent;
|
||||
if (!lc)
|
||||
return;
|
||||
|
||||
const query = _pendingQuery;
|
||||
const mode = _pendingMode || SessionData.appDrawerLastMode || "apps";
|
||||
_pendingMode = "";
|
||||
_pendingQuery = "";
|
||||
|
||||
if (lc.searchField) {
|
||||
lc.searchField.text = "";
|
||||
lc.searchField.text = query;
|
||||
lc.searchField.forceActiveFocus();
|
||||
}
|
||||
if (lc.controller) {
|
||||
lc.controller.searchMode = "apps";
|
||||
lc.controller.searchMode = mode;
|
||||
lc.controller.pluginFilter = "";
|
||||
lc.controller.searchQuery = "";
|
||||
lc.controller.performSearch();
|
||||
if (query) {
|
||||
lc.controller.setSearchQuery(query);
|
||||
} else {
|
||||
lc.controller.performSearch();
|
||||
}
|
||||
}
|
||||
lc.resetScroll?.();
|
||||
lc.actionPanel?.hide();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: contentLoader.item?.launcherContent?.controller ?? null
|
||||
function onModeChanged(mode) {
|
||||
if (contentLoader.item.launcherContent.controller.autoSwitchedToFiles)
|
||||
return;
|
||||
SessionData.setAppDrawerLastMode(mode);
|
||||
}
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Rectangle {
|
||||
id: contentContainer
|
||||
|
||||
@@ -85,12 +85,12 @@ Variants {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (typeof blurWallpaperWindow.updatesEnabled !== "undefined")
|
||||
blurWallpaperWindow.updatesEnabled = Qt.binding(() => root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading);
|
||||
|
||||
if (!source) {
|
||||
isInitialized = true;
|
||||
return;
|
||||
root._renderSettling = false;
|
||||
}
|
||||
const formattedSource = source.startsWith("file://") ? source : encodeFileUrl(source);
|
||||
setWallpaperImmediate(formattedSource);
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
@@ -98,8 +98,25 @@ Variants {
|
||||
property real transitionProgress: 0
|
||||
readonly property bool transitioning: transitionAnimation.running
|
||||
property bool effectActive: false
|
||||
property bool _renderSettling: true
|
||||
property bool useNextForEffect: false
|
||||
|
||||
Connections {
|
||||
target: currentWallpaper
|
||||
function onStatusChanged() {
|
||||
if (currentWallpaper.status === Image.Ready) {
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: renderSettleTimer
|
||||
interval: 100
|
||||
onTriggered: root._renderSettling = false
|
||||
}
|
||||
|
||||
onSourceChanged: {
|
||||
if (!source || source.startsWith("#")) {
|
||||
setWallpaperImmediate("");
|
||||
@@ -124,6 +141,8 @@ Variants {
|
||||
transitionAnimation.stop();
|
||||
root.transitionProgress = 0.0;
|
||||
root.effectActive = false;
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
currentWallpaper.source = newSource;
|
||||
nextWallpaper.source = "";
|
||||
}
|
||||
|
||||
@@ -15,18 +15,22 @@ Item {
|
||||
property var pluginDetailInstance: null
|
||||
property var widgetModel: null
|
||||
property var collapseCallback: null
|
||||
property real maxAvailableHeight: 9999
|
||||
|
||||
function getDetailHeight(section) {
|
||||
const maxAvailable = parent ? parent.height - Theme.spacingS : 9999;
|
||||
switch (true) {
|
||||
case section === "wifi":
|
||||
case section === "bluetooth":
|
||||
case section === "builtin_vpn":
|
||||
return Math.min(350, maxAvailable);
|
||||
return Math.min(350, maxAvailableHeight);
|
||||
case section.startsWith("brightnessSlider_"):
|
||||
return Math.min(400, maxAvailable);
|
||||
return Math.min(400, maxAvailableHeight);
|
||||
case section.startsWith("plugin_"):
|
||||
if (pluginDetailInstance?.ccDetailHeight)
|
||||
return Math.min(pluginDetailInstance.ccDetailHeight, maxAvailableHeight);
|
||||
return Math.min(250, maxAvailableHeight);
|
||||
default:
|
||||
return Math.min(250, maxAvailable);
|
||||
return Math.min(250, maxAvailableHeight);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,11 +31,26 @@ Column {
|
||||
|
||||
spacing: editMode ? Theme.spacingL : Theme.spacingS
|
||||
|
||||
property real maxPopoutHeight: 9999
|
||||
property var currentRowWidgets: []
|
||||
property real currentRowWidth: 0
|
||||
property int expandedRowIndex: -1
|
||||
property var colorPickerModal: null
|
||||
|
||||
readonly property real _maxDetailHeight: {
|
||||
const rows = layoutResult.rows;
|
||||
let totalRowHeight = 0;
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const sliderOnly = rows[i].every(w => {
|
||||
const id = w.id || "";
|
||||
return id === "volumeSlider" || id === "brightnessSlider" || id === "inputVolumeSlider";
|
||||
});
|
||||
totalRowHeight += sliderOnly ? 36 : 60;
|
||||
}
|
||||
const rowSpacing = Math.max(0, rows.length - 1) * spacing;
|
||||
return Math.max(100, maxPopoutHeight - totalRowHeight - rowSpacing);
|
||||
}
|
||||
|
||||
function calculateRowsAndWidgets() {
|
||||
return LayoutUtils.calculateRowsAndWidgets(root, expandedSection, expandedWidgetIndex);
|
||||
}
|
||||
@@ -163,6 +178,7 @@ Column {
|
||||
DetailHost {
|
||||
id: detailHost
|
||||
width: parent.width
|
||||
maxAvailableHeight: root._maxDetailHeight
|
||||
height: active ? (getDetailHeight(root.expandedSection) + Theme.spacingS) : 0
|
||||
property bool active: {
|
||||
if (root.expandedSection === "")
|
||||
@@ -945,22 +961,31 @@ Column {
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(() => {
|
||||
const pluginComponent = PluginService.pluginWidgetComponents[pluginId];
|
||||
if (pluginComponent) {
|
||||
const instance = pluginComponent.createObject(null, {
|
||||
"pluginId": pluginId,
|
||||
"pluginService": PluginService,
|
||||
"visible": false,
|
||||
"width": 0,
|
||||
"height": 0
|
||||
});
|
||||
if (instance) {
|
||||
pluginInstance = instance;
|
||||
}
|
||||
function tryCreatePluginInstance() {
|
||||
const pluginComponent = PluginService.pluginWidgetComponents[pluginId];
|
||||
if (!pluginComponent)
|
||||
return false;
|
||||
try {
|
||||
const instance = pluginComponent.createObject(null, {
|
||||
"pluginId": pluginId,
|
||||
"pluginService": PluginService,
|
||||
"visible": false,
|
||||
"width": 0,
|
||||
"height": 0
|
||||
});
|
||||
if (instance) {
|
||||
pluginInstance = instance;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn("DragDropGrid: stale plugin component for", pluginId, "- reloading");
|
||||
PluginService.reloadPlugin(pluginId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(() => tryCreatePluginInstance());
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -970,6 +995,11 @@ Column {
|
||||
pluginInstance.loadPluginData();
|
||||
}
|
||||
}
|
||||
function onPluginLoaded(loadedPluginId) {
|
||||
if (loadedPluginId !== pluginId || pluginInstance)
|
||||
return;
|
||||
Qt.callLater(() => tryCreatePluginInstance());
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
|
||||
@@ -13,8 +13,8 @@ Row {
|
||||
property Item popoutContent: null
|
||||
|
||||
signal addWidget(string widgetId)
|
||||
signal resetToDefault()
|
||||
signal clearAll()
|
||||
signal resetToDefault
|
||||
signal clearAll
|
||||
|
||||
height: 48
|
||||
spacing: Theme.spacingS
|
||||
@@ -28,7 +28,7 @@ Row {
|
||||
y: parent ? Math.round((parent.height - height) / 2) : 0
|
||||
width: 400
|
||||
height: 300
|
||||
modal: true
|
||||
modal: false
|
||||
focus: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
@@ -133,7 +133,7 @@ Row {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.addWidget(modelData.id)
|
||||
root.addWidget(modelData.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user