1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-15 08:42:47 -04:00

Compare commits

..

1 Commits

Author SHA1 Message Date
bbedward 22f384b821 blur: demo BackgroundEffect.blurRegion on some components 2026-02-16 09:47:30 -05:00
301 changed files with 8307 additions and 27126 deletions
+20 -53
View File
@@ -7,32 +7,32 @@ body:
attributes: attributes:
value: | value: |
## DankMaterialShell Bug Report ## DankMaterialShell Bug Report
Limit your report to one issue per submission unless similarly related Limit your report to one issue per submission unless closely related
- type: dropdown - type: checkboxes
id: compositor id: compositor
attributes: attributes:
label: Compositor label: Compositor
options: options:
- Niri - label: Niri
- Hyprland - label: Hyprland
- MangoWC (dwl) - label: MangoWC (dwl)
- Sway - label: Sway
validations: validations:
required: true required: true
- type: dropdown - type: checkboxes
id: distribution id: distribution
attributes: attributes:
label: Distribution label: Distribution
options: options:
- Arch Linux - label: Arch Linux
- CachyOS - label: CachyOS
- Fedora - label: Fedora
- NixOS - label: NixOS
- Debian - label: Debian
- Ubuntu - label: Ubuntu
- Gentoo - label: Gentoo
- OpenSUSE - label: OpenSUSE
- Other (specify below) - label: Other (specify below)
validations: validations:
required: true required: true
- type: input - type: input
@@ -42,45 +42,12 @@ body:
placeholder: e.g., PikaOS, Void Linux, etc. placeholder: e.g., PikaOS, Void Linux, etc.
validations: validations:
required: false required: false
- type: dropdown
id: installation_method
attributes:
label: Select your Installation Method
options:
- DankInstaller
- Distro Packaging
- Source
validations:
required: true
- type: dropdown
id: original_installation_method
attributes:
label: Was this your original Installation method?
options:
- "Yes"
- No (specify below)
default: 0
validations:
required: false
- type: input
id: original_installation_method_specify
attributes:
label: If no, specify
placeholder: e.g., Distro Packaging, then Source
validations:
required: false
- type: textarea - type: textarea
id: dms_doctor id: dms_doctor
attributes: attributes:
label: dms doctor -vC label: dms doctor -v
description: Output of `dms doctor -vC` command — paste between the details tags below to keep it collapsed in the issue description: Output of `dms doctor -v` command
placeholder: Paste the output of `dms doctor -vC` here placeholder: Paste the output of `dms doctor -v` here
value: |
<details>
<summary>Click to expand</summary>
</details>
validations: validations:
required: true required: true
- type: textarea - type: textarea
@@ -102,7 +69,7 @@ body:
- type: textarea - type: textarea
id: steps_to_reproduce id: steps_to_reproduce
attributes: attributes:
label: Steps to Reproduce label: Steps to Reproduce & Installation Method
description: Please provide detailed steps to reproduce the issue description: Please provide detailed steps to reproduce the issue
placeholder: | placeholder: |
1. ... 1. ...
+7 -14
View File
@@ -23,25 +23,18 @@ body:
placeholder: Why is this feature important? placeholder: Why is this feature important?
validations: validations:
required: false required: false
- type: dropdown - type: checkboxes
id: compositor id: compositor
attributes: attributes:
label: Compositor(s) label: Compositor(s)
description: Is this feature specific to one or more compositors? description: Is this feature specific to one or more compositors?
options: options:
- All compositors - label: All compositors
- Niri - label: Niri
- Hyprland - label: Hyprland
- MangoWC (dwl) - label: MangoWC (dwl)
- Sway - label: Sway
- Other (specify below) - label: Other (specify below)
validations:
required: true
- type: input
id: compositor_other
attributes:
label: If Other, please specify
placeholder: e.g., Wayfire, Mutter, etc.
validations: validations:
required: false required: false
- type: textarea - type: textarea
+12 -67
View File
@@ -7,87 +7,32 @@ body:
attributes: attributes:
value: | value: |
## DankMaterialShell Support Request ## DankMaterialShell Support Request
- type: dropdown - type: checkboxes
id: compositor id: compositor
attributes: attributes:
label: Compositor label: Compositor
options: options:
- Niri - label: Niri
- Hyprland - label: Hyprland
- MangoWC (dwl) - label: MangoWC (dwl)
- Sway - label: Sway
- Other (specify below) - label: Other (specify below)
validations:
required: true
- type: input
id: compositor_other
attributes:
label: If Other, please specify
placeholder: e.g., Wayfire, Mutter, etc.
validations: validations:
required: false required: false
- type: dropdown - type: input
id: distribution id: distribution
attributes: attributes:
label: Distribution label: Distribution
options: description: Which Linux distribution are you using? (e.g., Arch, Fedora, Debian, etc.)
- Arch Linux placeholder: Your Linux distribution
- CachyOS
- Fedora
- NixOS
- Debian
- Ubuntu
- Gentoo
- OpenSUSE
- Other (specify below)
validations:
required: true
- type: input
id: distribution_other
attributes:
label: If Other, please specify
placeholder: e.g., PikaOS, Void Linux, etc.
validations:
required: false
- type: dropdown
id: installation_method
attributes:
label: Select your Installation Method
options:
- DankInstaller
- Distro Packaging
- Source
validations:
required: true
- type: dropdown
id: original_installation_method_different
attributes:
label: Was your original Installation method different?
options:
- "Yes"
- No (specify below)
default: 0
validations:
required: false
- type: input
id: original_installation_method_specify
attributes:
label: If no, specify
placeholder: e.g., Distro Packaging, then Source
validations: validations:
required: false required: false
- type: textarea - type: textarea
id: dms_doctor id: dms_doctor
attributes: attributes:
label: dms doctor -vC label: dms doctor -v
description: Output of `dms doctor -vC` command — paste between the lines below to keep it collapsed in the issue description: Output of `dms doctor -v` command
placeholder: Paste the output of `dms doctor -vC` here placeholder: Paste the output of `dms doctor -v` here
value: |
<details>
<summary>Click to expand</summary>
</details>
validations: validations:
required: false required: false
- type: textarea - type: textarea
+383
View File
@@ -0,0 +1,383 @@
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
View File
@@ -0,0 +1,298 @@
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
+82 -163
View File
@@ -10,7 +10,6 @@ on:
options: options:
- dms - dms
- dms-git - dms-git
- dms-greeter
- all - all
default: "dms" default: "dms"
rebuild_release: rebuild_release:
@@ -73,27 +72,12 @@ jobs:
fi fi
} }
# Helper function to check dms-greeter stable tag
check_dms_greeter_stable() {
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:danklinux/dms-greeter/dms-greeter.spec" 2>/dev/null || echo "")
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs | sed 's/^v//')
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "v$OBS_VERSION" ]]; then
echo "📋 dms-greeter: Tag $LATEST_TAG already exists, skipping"
return 1 # No update needed
else
echo "📋 dms-greeter: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
return 0 # Update needed
fi
}
# Main logic # Main logic
REBUILD="${{ github.event.inputs.rebuild_release }}" REBUILD="${{ github.event.inputs.rebuild_release }}"
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
# Run from tag with no package specified - update both stable packages # Tag selected or pushed - always update stable package
echo "packages=dms dms-greeter" >> $GITHUB_OUTPUT echo "packages=dms" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}" VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT echo "has_updates=true" >> $GITHUB_OUTPUT
@@ -128,10 +112,6 @@ jobs:
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi fi
fi fi
if check_dms_greeter_stable; then
PACKAGES_TO_UPDATE+=("dms-greeter")
[[ -n "$LATEST_TAG" ]] && echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
@@ -164,18 +144,6 @@ jobs:
echo "has_updates=false" >> $GITHUB_OUTPUT echo "has_updates=false" >> $GITHUB_OUTPUT
fi fi
elif [[ "$PKG" == "dms-greeter" ]]; then
if check_dms_greeter_stable; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
else else
# Unknown package - proceed anyway # Unknown package - proceed anyway
echo "packages=$PKG" >> $GITHUB_OUTPUT echo "packages=$PKG" >> $GITHUB_OUTPUT
@@ -203,8 +171,15 @@ jobs:
- name: Determine packages to update - name: Determine packages to update
id: packages id: packages
run: | run: |
# Use check-updates outputs when available # Check if GITHUB_REF points to a tag (works for both push events and workflow_dispatch with tag selected)
if [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then 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 version from check-updates job # Use version from check-updates job
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
@@ -216,16 +191,40 @@ jobs:
elif [[ -n "${{ github.event.inputs.package }}" ]]; then elif [[ -n "${{ github.event.inputs.package }}" ]]; then
# Manual workflow dispatch # Manual workflow dispatch
# Determine version for dms stable and dms-greeter using the API # Determine version for dms stable
# GITHUB_REF is unreliable when "Use workflow from" a tag; API works from any ref if [[ "${{ github.event.inputs.package }}" == "dms" ]]; then
if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "dms-greeter" ]] || [[ "${{ github.event.inputs.package }}" == "all" ]]; then # Use github.ref if tag selected, otherwise auto-detect latest
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "") if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
if [[ -n "$LATEST_TAG" ]]; then VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Using latest release from API: $LATEST_TAG" echo "Using tag from GITHUB_REF: $VERSION"
else else
echo "ERROR: Could not fetch latest release from API" # Auto-detect latest release for dms
exit 1 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
fi fi
fi fi
@@ -284,66 +283,55 @@ jobs:
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE" echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
} > "distro/debian/dms-git/debian/changelog" } > "distro/debian/dms-git/debian/changelog"
- name: Update stable version (dms + dms-greeter) - name: Update dms stable version
if: steps.packages.outputs.version != '' if: steps.packages.outputs.version != ''
run: | run: |
VERSION="${{ steps.packages.outputs.version }}" VERSION="${{ steps.packages.outputs.version }}"
VERSION_NO_V="${VERSION#v}" VERSION_NO_V="${VERSION#v}"
PACKAGES="${{ steps.packages.outputs.packages }}"
echo "==> Updating packaging files to version: $VERSION_NO_V" echo "==> Updating packaging files to version: $VERSION_NO_V"
# Update dms spec and changelog when dms is in the upload list # Update spec file
if [[ "$PACKAGES" == *"dms"* ]]; then sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
UPDATED_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1)
echo "✓ dms spec now shows Version: $UPDATED_VERSION"
DATE_STR=$(date "+%a %b %d %Y") # Verify the update
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms.spec) UPDATED_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1)
{ echo "✓ Spec file now shows Version: $UPDATED_VERSION"
echo "$LOCAL_SPEC_HEAD"
echo "%changelog"
echo "* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1"
echo "- Update to stable $VERSION release"
} > distro/opensuse/dms.spec
if [[ -f "distro/debian/dms/debian/changelog" ]]; then # Single changelog entry (full history on OBS website)
CHANGELOG_DATE=$(date -R) DATE_STR=$(date "+%a %b %d %Y")
{ LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms.spec)
echo "dms (${VERSION_NO_V}db1) stable; urgency=medium" {
echo "" echo "$LOCAL_SPEC_HEAD"
echo " * Update to $VERSION stable release" echo "%changelog"
echo "" echo "* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1"
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE" echo "- Update to stable $VERSION release"
} > "distro/debian/dms/debian/changelog" } > distro/opensuse/dms.spec
echo "✓ Updated dms changelog to ${VERSION_NO_V}db1"
fi
fi
# Update dms-greeter changelog when dms-greeter is in the upload list # Update Debian _service files (both tar_scm and download_url formats)
if [[ "$PACKAGES" == *"dms-greeter"* ]] && [[ -f "distro/debian/dms-greeter/debian/changelog" ]]; then
CHANGELOG_DATE=$(date -R)
{
echo "dms-greeter (${VERSION_NO_V}db1) unstable; urgency=medium"
echo ""
echo " * Update to $VERSION stable release"
echo ""
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
} > "distro/debian/dms-greeter/debian/changelog"
echo "✓ Updated dms-greeter changelog to ${VERSION_NO_V}db1"
fi
# Update Debian _service files for packages in upload list (download_url paths)
for service in distro/debian/*/_service; do for service in distro/debian/*/_service; do
if [[ -f "$service" ]]; then if [[ -f "$service" ]]; then
# Update tar_scm revision parameter (for dms-git) # Update tar_scm revision parameter (for dms-git)
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service" sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
# Update download_url paths (for dms, dms-greeter stable)
# Update download_url paths (for dms stable)
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service" sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service" sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
fi fi
done done
# Update Debian changelog for dms stable (single entry, history on OBS website)
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
CHANGELOG_DATE=$(date -R)
{
echo "dms (${VERSION_NO_V}db1) stable; urgency=medium"
echo ""
echo " * Update to $VERSION stable release"
echo ""
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
} > "distro/debian/dms/debian/changelog"
echo "✓ Updated Debian changelog to ${VERSION_NO_V}db1"
fi
- name: Install Go - name: Install Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
@@ -366,7 +354,6 @@ jobs:
chmod 600 ~/.config/osc/oscrc chmod 600 ~/.config/osc/oscrc
- name: Upload to OBS - name: Upload to OBS
id: upload
env: env:
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }} REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
TAG_VERSION: ${{ steps.packages.outputs.version }} TAG_VERSION: ${{ steps.packages.outputs.version }}
@@ -375,8 +362,6 @@ jobs:
if [[ -z "$PACKAGES" ]]; then if [[ -z "$PACKAGES" ]]; then
echo "✓ No packages need uploading. All up to date!" echo "✓ No packages need uploading. All up to date!"
echo "uploaded_packages=" >> $GITHUB_OUTPUT
echo "skipped_packages=" >> $GITHUB_OUTPUT
exit 0 exit 0
fi fi
@@ -386,9 +371,6 @@ jobs:
echo "==> Version being uploaded: ${{ steps.packages.outputs.version }}" echo "==> Version being uploaded: ${{ steps.packages.outputs.version }}"
fi fi
UPLOADED_PACKAGES=()
SKIPPED_PACKAGES=()
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check) # PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
# Loop through each package and upload # Loop through each package and upload
for PKG in $PACKAGES; do for PKG in $PACKAGES; do
@@ -400,37 +382,13 @@ jobs:
fi fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
LOG_FILE=$(mktemp)
set +e
if [[ "$PKG" == "dms-git" ]]; then if [[ "$PKG" == "dms-git" ]]; then
bash distro/scripts/obs-upload.sh dms-git "Automated git update" >"$LOG_FILE" 2>&1 bash distro/scripts/obs-upload.sh dms-git "Automated git update"
else else
bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE" >"$LOG_FILE" 2>&1 bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE"
fi fi
STATUS=$?
set -e
cat "$LOG_FILE"
if [[ $STATUS -ne 0 ]]; then
rm -f "$LOG_FILE"
echo "❌ Upload failed for $PKG"
exit $STATUS
fi
if grep -Eq "Exiting gracefully \(no changes needed\)|No changes needed for this package\. Exiting gracefully\." "$LOG_FILE"; then
echo "️ $PKG is already up to date. Skipped."
SKIPPED_PACKAGES+=("$PKG")
else
UPLOADED_PACKAGES+=("$PKG")
fi
rm -f "$LOG_FILE"
done done
echo "uploaded_packages=${UPLOADED_PACKAGES[*]}" >> $GITHUB_OUTPUT
echo "skipped_packages=${SKIPPED_PACKAGES[*]}" >> $GITHUB_OUTPUT
- name: Summary - name: Summary
if: always() if: always()
run: | run: |
@@ -444,59 +402,20 @@ jobs:
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "All packages are current. Run completed successfully." >> $GITHUB_STEP_SUMMARY echo "All packages are current. Run completed successfully." >> $GITHUB_STEP_SUMMARY
else else
UPLOADED_PACKAGES="${{ steps.upload.outputs.uploaded_packages }}" echo "**Packages Uploaded:**" >> $GITHUB_STEP_SUMMARY
SKIPPED_PACKAGES="${{ steps.upload.outputs.skipped_packages }}"
TOTAL_COUNT=$(wc -w <<<"$PACKAGES" | tr -d ' ')
UPLOADED_COUNT=0
SKIPPED_COUNT=0
if [[ -n "$UPLOADED_PACKAGES" ]]; then
UPLOADED_COUNT=$(wc -w <<<"$UPLOADED_PACKAGES" | tr -d ' ')
fi
if [[ -n "$SKIPPED_PACKAGES" ]]; then
SKIPPED_COUNT=$(wc -w <<<"$SKIPPED_PACKAGES" | tr -d ' ')
fi
in_list() {
local item="$1"
local list="$2"
[[ " $list " == *" $item "* ]]
}
if [[ "${{ job.status }}" == "success" ]]; then
echo "**Status:** ✅ Completed successfully" >> $GITHUB_STEP_SUMMARY
else
echo "**Status:** ❌ Completed with errors" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Processed:** $TOTAL_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
echo "**Uploaded:** $UPLOADED_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
echo "**Skipped (up to date):** $SKIPPED_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Packages:**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
for PKG in $PACKAGES; do for PKG in $PACKAGES; do
STATUS_ICON="✅"
STATUS_TEXT="uploaded"
if in_list "$PKG" "$SKIPPED_PACKAGES"; then
STATUS_ICON="️"
STATUS_TEXT="up to date (skipped)"
elif ! in_list "$PKG" "$UPLOADED_PACKAGES"; then
STATUS_ICON="❌"
STATUS_TEXT="failed"
fi
case "$PKG" in case "$PKG" in
dms) dms)
echo "- $STATUS_ICON **dms** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms/dms)" >> $GITHUB_STEP_SUMMARY echo "- ✅ **dms** → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms/dms)" >> $GITHUB_STEP_SUMMARY
;; ;;
dms-git) dms-git)
echo "- $STATUS_ICON **dms-git** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY echo "- ✅ **dms-git** → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
;;
dms-greeter)
echo "- $STATUS_ICON **dms-greeter** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:danklinux/dms-greeter)" >> $GITHUB_STEP_SUMMARY
;; ;;
esac esac
done done
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
+3 -3
View File
@@ -40,7 +40,7 @@ jobs:
echo "Build succeeded, no hash update needed" echo "Build succeeded, no hash update needed"
exit 0 exit 0
fi fi
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1 || true) new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1)
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; } [ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix) current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; } [ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
@@ -59,8 +59,8 @@ jobs:
git config user.email "dms-ci[bot]@users.noreply.github.com" git config user.email "dms-ci[bot]@users.noreply.github.com"
git add flake.nix git add flake.nix
git commit -m "nix: update vendorHash for go.mod changes" || exit 0 git commit -m "nix: update vendorHash for go.mod changes" || exit 0
git pull --rebase origin ${{ github.ref_name }} git pull --rebase origin master
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:${{ github.ref_name }} git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
else else
echo "No changes to flake.nix" echo "No changes to flake.nix"
fi fi
+3 -5
View File
@@ -5,13 +5,11 @@ repos:
- id: trailing-whitespace - id: trailing-whitespace
- id: check-yaml - id: check-yaml
- id: end-of-file-fixer - id: end-of-file-fixer
- repo: local - repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.10.0.1
hooks: hooks:
- id: shellcheck - id: shellcheck
name: shellcheck args: [-e, SC2164, -e, SC2001, -e, SC2012, -e, SC2317]
entry: shellcheck -e SC2164 -e SC2001 -e SC2012 -e SC2317
language: system
types: [shell]
- repo: local - repo: local
hooks: hooks:
- id: go-mod-tidy - id: go-mod-tidy
+1 -1
View File
@@ -22,7 +22,7 @@ nix develop
This will provide: This will provide:
- Go 1.25+ toolchain (go, gopls, delve, go-tools) and GNU Make - Go 1.24 toolchain (go, gopls, delve, go-tools) and GNU Make
- Quickshell and required QML packages - Quickshell and required QML packages
- Properly configured QML2_IMPORT_PATH - Properly configured QML2_IMPORT_PATH
+2 -2
View File
@@ -19,7 +19,7 @@ Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
</div> </div>
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), [Miracle WM](https://github.com/miracle-wm-org/miracle-wm), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop. DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop.
## Repository Structure ## Repository Structure
@@ -105,7 +105,7 @@ Extend functionality with the [plugin registry](https://plugins.danklinux.com).
## Supported Compositors ## Supported Compositors
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), and [Miracle WM](https://github.com/miracle-wm-org/miracle-wm) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features. Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [labwc](https://labwc.github.io/), and [Scroll](https://github.com/dawsers/scroll) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features.
[Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors) [Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors)
+1 -1
View File
@@ -96,7 +96,7 @@ The on-screen preview displays the selected format. JSON output includes hex, RG
## Building ## Building
Requires Go 1.25+ Requires Go 1.24+
**Development build:** **Development build:**
@@ -1,10 +0,0 @@
{
"policy_version": 1,
"blocked_commands": [
"greeter install",
"greeter enable",
"greeter uninstall",
"setup"
],
"message": "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes."
}
+1 -11
View File
@@ -222,19 +222,16 @@ func init() {
func runClipCopy(cmd *cobra.Command, args []string) { func runClipCopy(cmd *cobra.Command, args []string) {
var data []byte var data []byte
copyFromStdin := false
switch { switch {
case len(args) > 0: case len(args) > 0:
data = []byte(args[0]) data = []byte(args[0])
case clipCopyDownload || clipCopyType == "__multi__": default:
var err error var err error
data, err = io.ReadAll(os.Stdin) data, err = io.ReadAll(os.Stdin)
if err != nil { if err != nil {
log.Fatalf("read stdin: %v", err) log.Fatalf("read stdin: %v", err)
} }
default:
copyFromStdin = true
} }
if clipCopyDownload { if clipCopyDownload {
@@ -260,13 +257,6 @@ func runClipCopy(cmd *cobra.Command, args []string) {
return return
} }
if copyFromStdin {
if err := clipboard.CopyReader(os.Stdin, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
log.Fatalf("copy: %v", err)
}
return
}
if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil { if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
log.Fatalf("copy: %v", err) log.Fatalf("copy: %v", err)
} }
-1
View File
@@ -525,6 +525,5 @@ func getCommonCommands() []*cobra.Command {
doctorCmd, doctorCmd,
configCmd, configCmd,
dlCmd, dlCmd,
randrCmd,
} }
} }
+38 -82
View File
@@ -649,104 +649,58 @@ func checkI2CAvailability() checkResult {
return checkResult{catOptionalFeatures, "I2C/DDC", statusOK, fmt.Sprintf("%d monitor(s) detected", len(devices)), "External monitor brightness control", doctorDocsURL + "#optional-features"} return checkResult{catOptionalFeatures, "I2C/DDC", statusOK, fmt.Sprintf("%d monitor(s) detected", len(devices)), "External monitor brightness control", doctorDocsURL + "#optional-features"}
} }
func checkImageFormatPlugins() []checkResult { func checkKImageFormats() checkResult {
url := doctorDocsURL + "#optional-features" url := doctorDocsURL + "#optional-features"
desc := "Extra image format support (AVIF, HEIF, JXL)"
pluginDirs := findQtPluginDirs() pluginDir := findQtPluginDir()
if len(pluginDirs) == 0 { if pluginDir == "" {
return []checkResult{ return checkResult{catOptionalFeatures, "kimageformats", statusInfo, "Cannot detect (qtpaths not found)", desc, url}
{catOptionalFeatures, "qt6-imageformats", statusInfo, "Cannot detect (plugin dir not found)", "WebP, TIFF, JP2 support", url}, }
{catOptionalFeatures, "kimageformats", statusInfo, "Cannot detect (plugin dir not found)", "AVIF, HEIF, JXL support", url},
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)
} }
} }
type pluginCheck struct { if len(found) == 0 {
name string return checkResult{catOptionalFeatures, "kimageformats", statusWarn, "Not installed", desc, url}
desc string
plugins []struct{ file, format string }
} }
checks := []pluginCheck{ details := ""
{ if doctorVerbose {
name: "qt6-imageformats", details = fmt.Sprintf("Formats: %s (%s)", strings.Join(found, ", "), imageFormatsDir)
desc: "WebP, TIFF, GIF, JP2 support",
plugins: []struct{ file, format string }{
{"libqwebp.so", "WebP"},
{"libqtiff.so", "TIFF"},
{"libqgif.so", "GIF"},
{"libqjp2.so", "JP2"},
{"libqicns.so", "ICNS"},
},
},
{
name: "kimageformats",
desc: "AVIF, HEIF, JXL support",
plugins: []struct{ file, format string }{
{"kimg_avif.so", "AVIF"},
{"kimg_heif.so", "HEIF"},
{"kimg_jxl.so", "JXL"},
{"kimg_exr.so", "EXR"},
},
},
} }
var results []checkResult return checkResult{catOptionalFeatures, "kimageformats", statusOK, fmt.Sprintf("Installed (%d formats)", len(found)), details, url}
for _, c := range checks {
var found []string
var foundDirs []string
for _, pluginDir := range pluginDirs {
imageFormatsDir := filepath.Join(pluginDir, "imageformats")
for _, p := range c.plugins {
if _, err := os.Stat(filepath.Join(imageFormatsDir, p.file)); err == nil {
if !slices.Contains(found, p.format) {
found = append(found, p.format)
}
if !slices.Contains(foundDirs, imageFormatsDir) {
foundDirs = append(foundDirs, imageFormatsDir)
}
}
}
}
var result checkResult
switch {
case len(found) == 0:
result = checkResult{catOptionalFeatures, c.name, statusWarn, "Not installed", c.desc, url}
default:
details := ""
if doctorVerbose {
details = fmt.Sprintf("Formats: %s (%s)", strings.Join(found, ", "), strings.Join(foundDirs, ":"))
}
result = checkResult{catOptionalFeatures, c.name, statusOK, fmt.Sprintf("Installed (%d formats)", len(found)), details, url}
}
results = append(results, result)
}
return results
} }
func findQtPluginDirs() []string { func findQtPluginDir() string {
var dirs []string // Check QT_PLUGIN_PATH env var first (used by NixOS and custom setups)
addDir := func(dir string) {
if dir != "" {
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
dirs = append(dirs, dir)
}
}
}
// Check all paths in QT_PLUGIN_PATH env var (used by NixOS and custom setups)
if envPath := os.Getenv("QT_PLUGIN_PATH"); envPath != "" { if envPath := os.Getenv("QT_PLUGIN_PATH"); envPath != "" {
for dir := range strings.SplitSeq(envPath, ":") { for dir := range strings.SplitSeq(envPath, ":") {
addDir(dir) if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
return dir
}
} }
} }
// Try qtpaths // Try qtpaths
for _, cmd := range []string{"qtpaths6", "qtpaths"} { for _, cmd := range []string{"qtpaths6", "qtpaths"} {
if output, err := exec.Command(cmd, "-query", "QT_INSTALL_PLUGINS").Output(); err == nil { if output, err := exec.Command(cmd, "-query", "QT_INSTALL_PLUGINS").Output(); err == nil {
addDir(strings.TrimSpace(string(output))) if dir := strings.TrimSpace(string(output)); dir != "" {
return dir
}
} }
} }
@@ -757,10 +711,12 @@ func findQtPluginDirs() []string {
"/usr/lib/x86_64-linux-gnu/qt6/plugins", "/usr/lib/x86_64-linux-gnu/qt6/plugins",
"/usr/lib/aarch64-linux-gnu/qt6/plugins", "/usr/lib/aarch64-linux-gnu/qt6/plugins",
} { } {
addDir(dir) if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
return dir
}
} }
return dirs return ""
} }
func detectNetworkBackend(stackResult *network.DetectResult) string { func detectNetworkBackend(stackResult *network.DetectResult) string {
@@ -817,7 +773,7 @@ func checkOptionalDependencies() []checkResult {
results = append(results, checkResult{catOptionalFeatures, "cups-pk-helper", cupsPkStatus, cupsPkMsg, "Printer management", optionalFeaturesURL}) results = append(results, checkResult{catOptionalFeatures, "cups-pk-helper", cupsPkStatus, cupsPkMsg, "Printer management", optionalFeaturesURL})
results = append(results, checkI2CAvailability()) results = append(results, checkI2CAvailability())
results = append(results, checkImageFormatPlugins()...) results = append(results, checkKImageFormats())
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"} terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 { if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
File diff suppressed because it is too large Load Diff
+8 -22
View File
@@ -4,7 +4,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"path/filepath"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds/providers" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds/providers"
@@ -83,35 +82,24 @@ func init() {
func initializeProviders() { func initializeProviders() {
registry := keybinds.GetDefaultRegistry() registry := keybinds.GetDefaultRegistry()
hyprlandProvider := providers.NewHyprlandProvider("") hyprlandProvider := providers.NewHyprlandProvider("$HOME/.config/hypr")
if err := registry.Register(hyprlandProvider); err != nil { if err := registry.Register(hyprlandProvider); err != nil {
log.Warnf("Failed to register Hyprland provider: %v", err) log.Warnf("Failed to register Hyprland provider: %v", err)
} }
mangowcProvider := providers.NewMangoWCProvider("") mangowcProvider := providers.NewMangoWCProvider("$HOME/.config/mango")
if err := registry.Register(mangowcProvider); err != nil { if err := registry.Register(mangowcProvider); err != nil {
log.Warnf("Failed to register MangoWC provider: %v", err) log.Warnf("Failed to register MangoWC provider: %v", err)
} }
configDir, _ := os.UserConfigDir() scrollProvider := providers.NewSwayProvider("$HOME/.config/scroll")
if err := registry.Register(scrollProvider); err != nil {
if configDir != "" { log.Warnf("Failed to register Scroll provider: %v", err)
scrollProvider := providers.NewSwayProvider(filepath.Join(configDir, "scroll"))
if err := registry.Register(scrollProvider); err != nil {
log.Warnf("Failed to register Scroll provider: %v", err)
}
} }
miracleProvider := providers.NewMiracleProvider("") swayProvider := providers.NewSwayProvider("$HOME/.config/sway")
if err := registry.Register(miracleProvider); err != nil { if err := registry.Register(swayProvider); err != nil {
log.Warnf("Failed to register Miracle WM provider: %v", err) log.Warnf("Failed to register Sway provider: %v", err)
}
if configDir != "" {
swayProvider := providers.NewSwayProvider(filepath.Join(configDir, "sway"))
if err := registry.Register(swayProvider); err != nil {
log.Warnf("Failed to register Sway provider: %v", err)
}
} }
niriProvider := providers.NewNiriProvider("") niriProvider := providers.NewNiriProvider("")
@@ -156,8 +144,6 @@ func makeProviderWithPath(name, path string) keybinds.Provider {
return providers.NewSwayProvider(path) return providers.NewSwayProvider(path)
case "scroll": case "scroll":
return providers.NewSwayProvider(path) return providers.NewSwayProvider(path)
case "miracle":
return providers.NewMiracleProvider(path)
case "niri": case "niri":
return providers.NewNiriProvider(path) return providers.NewNiriProvider(path)
default: default:
+6 -23
View File
@@ -3,9 +3,7 @@ package main
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"os"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -97,11 +95,7 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
func runMatugenGenerate(cmd *cobra.Command, args []string) { func runMatugenGenerate(cmd *cobra.Command, args []string) {
opts := buildMatugenOptions(cmd) opts := buildMatugenOptions(cmd)
err := matugen.Run(opts) if err := matugen.Run(opts); err != nil {
switch {
case errors.Is(err, matugen.ErrNoChanges):
os.Exit(2)
case err != nil:
log.Fatalf("Theme generation failed: %v", err) log.Fatalf("Theme generation failed: %v", err)
} }
} }
@@ -135,11 +129,7 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
if !wait { if !wait {
if err := sendServerRequestFireAndForget(request); err != nil { if err := sendServerRequestFireAndForget(request); err != nil {
log.Info("Server unavailable, running synchronously") log.Info("Server unavailable, running synchronously")
err := matugen.Run(opts) if err := matugen.Run(opts); err != nil {
switch {
case errors.Is(err, matugen.ErrNoChanges):
os.Exit(2)
case err != nil:
log.Fatalf("Theme generation failed: %v", err) log.Fatalf("Theme generation failed: %v", err)
} }
return return
@@ -156,15 +146,11 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
resp, ok := tryServerRequest(request) resp, ok := tryServerRequest(request)
if !ok { if !ok {
log.Info("Server unavailable, running synchronously") log.Info("Server unavailable, running synchronously")
err := matugen.Run(opts) if err := matugen.Run(opts); err != nil {
switch {
case errors.Is(err, matugen.ErrNoChanges):
resultCh <- matugen.ErrNoChanges
case err != nil:
resultCh <- err resultCh <- err
default: return
resultCh <- nil
} }
resultCh <- nil
return return
} }
if resp.Error != "" { if resp.Error != "" {
@@ -176,10 +162,7 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
select { select {
case err := <-resultCh: case err := <-resultCh:
switch { if err != nil {
case errors.Is(err, matugen.ErrNoChanges):
os.Exit(2)
case err != nil:
log.Fatalf("Theme generation failed: %v", err) log.Fatalf("Theme generation failed: %v", err)
} }
fmt.Println("Theme generation completed") fmt.Println("Theme generation completed")
-58
View File
@@ -1,58 +0,0 @@
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 -15
View File
@@ -13,16 +13,16 @@ import (
) )
var ( var (
ssOutputName string ssOutputName string
ssCursor string ssIncludeCursor bool
ssFormat string ssFormat string
ssQuality int ssQuality int
ssOutputDir string ssOutputDir string
ssFilename string ssFilename string
ssNoClipboard bool ssNoClipboard bool
ssNoFile bool ssNoFile bool
ssNoNotify bool ssNoNotify bool
ssStdout bool ssStdout bool
) )
var screenshotCmd = &cobra.Command{ var screenshotCmd = &cobra.Command{
@@ -52,7 +52,7 @@ Examples:
dms screenshot last # Last region (pre-selected) dms screenshot last # Last region (pre-selected)
dms screenshot --no-clipboard # Save file only dms screenshot --no-clipboard # Save file only
dms screenshot --no-file # Clipboard only dms screenshot --no-file # Clipboard only
dms screenshot --cursor=on # Include cursor dms screenshot --cursor # Include cursor
dms screenshot -f jpg -q 85 # JPEG with quality 85`, dms screenshot -f jpg -q 85 # JPEG with quality 85`,
} }
@@ -111,7 +111,7 @@ var notifyActionCmd = &cobra.Command{
func init() { func init() {
screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode") screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode")
screenshotCmd.PersistentFlags().StringVar(&ssCursor, "cursor", "off", "Include cursor in screenshot (on/off)") screenshotCmd.PersistentFlags().BoolVar(&ssIncludeCursor, "cursor", false, "Include cursor in screenshot")
screenshotCmd.PersistentFlags().StringVarP(&ssFormat, "format", "f", "png", "Output format (png, jpg, ppm)") screenshotCmd.PersistentFlags().StringVarP(&ssFormat, "format", "f", "png", "Output format (png, jpg, ppm)")
screenshotCmd.PersistentFlags().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)") screenshotCmd.PersistentFlags().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)")
screenshotCmd.PersistentFlags().StringVarP(&ssOutputDir, "dir", "d", "", "Output directory") screenshotCmd.PersistentFlags().StringVarP(&ssOutputDir, "dir", "d", "", "Output directory")
@@ -136,9 +136,7 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
config := screenshot.DefaultConfig() config := screenshot.DefaultConfig()
config.Mode = mode config.Mode = mode
config.OutputName = ssOutputName config.OutputName = ssOutputName
if strings.EqualFold(ssCursor, "on") { config.IncludeCursor = ssIncludeCursor
config.Cursor = screenshot.CursorOn
}
config.Clipboard = !ssNoClipboard config.Clipboard = !ssNoClipboard
config.SaveFile = !ssNoFile config.SaveFile = !ssNoFile
config.Notify = !ssNoNotify config.Notify = !ssNoNotify
+3 -4
View File
@@ -16,10 +16,9 @@ import (
) )
var setupCmd = &cobra.Command{ var setupCmd = &cobra.Command{
Use: "setup", Use: "setup",
Short: "Deploy DMS configurations", Short: "Deploy DMS configurations",
Long: "Deploy compositor and terminal configurations with interactive prompts", Long: "Deploy compositor and terminal configurations with interactive prompts",
PersistentPreRunE: requireMutableSystemCommand,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if err := runSetup(); err != nil { if err := runSetup(); err != nil {
log.Fatalf("Error during setup: %v", err) log.Fatalf("Error during setup: %v", err)
-271
View File
@@ -1,271 +0,0 @@
package main
import (
"bufio"
_ "embed"
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"github.com/spf13/cobra"
)
const (
cliPolicyPackagedPath = "/usr/share/dms/cli-policy.json"
cliPolicyAdminPath = "/etc/dms/cli-policy.json"
)
var (
immutablePolicyOnce sync.Once
immutablePolicy immutableCommandPolicy
immutablePolicyErr error
)
//go:embed assets/cli-policy.default.json
var defaultCLIPolicyJSON []byte
type immutableCommandPolicy struct {
ImmutableSystem bool
ImmutableReason string
BlockedCommands []string
Message string
}
type cliPolicyFile struct {
PolicyVersion int `json:"policy_version"`
ImmutableSystem *bool `json:"immutable_system"`
BlockedCommands *[]string `json:"blocked_commands"`
Message *string `json:"message"`
}
func normalizeCommandSpec(raw string) string {
normalized := strings.ToLower(strings.TrimSpace(raw))
normalized = strings.TrimPrefix(normalized, "dms ")
return strings.Join(strings.Fields(normalized), " ")
}
func normalizeBlockedCommands(raw []string) []string {
normalized := make([]string, 0, len(raw))
seen := make(map[string]bool)
for _, cmd := range raw {
spec := normalizeCommandSpec(cmd)
if spec == "" || seen[spec] {
continue
}
seen[spec] = true
normalized = append(normalized, spec)
}
return normalized
}
func commandBlockedByPolicy(commandPath string, blocked []string) bool {
normalizedPath := normalizeCommandSpec(commandPath)
if normalizedPath == "" {
return false
}
for _, entry := range blocked {
spec := normalizeCommandSpec(entry)
if spec == "" {
continue
}
if normalizedPath == spec || strings.HasPrefix(normalizedPath, spec+" ") {
return true
}
}
return false
}
func loadPolicyFile(path string) (*cliPolicyFile, error) {
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("failed to read %s: %w", path, err)
}
var policy cliPolicyFile
if err := json.Unmarshal(data, &policy); err != nil {
return nil, fmt.Errorf("failed to parse %s: %w", path, err)
}
return &policy, nil
}
func mergePolicyFile(base *immutableCommandPolicy, path string) error {
policyFile, err := loadPolicyFile(path)
if err != nil {
return err
}
if policyFile == nil {
return nil
}
if policyFile.ImmutableSystem != nil {
base.ImmutableSystem = *policyFile.ImmutableSystem
}
if policyFile.BlockedCommands != nil {
base.BlockedCommands = normalizeBlockedCommands(*policyFile.BlockedCommands)
}
if policyFile.Message != nil {
msg := strings.TrimSpace(*policyFile.Message)
if msg != "" {
base.Message = msg
}
}
return nil
}
func readOSReleaseMap(path string) map[string]string {
values := make(map[string]string)
file, err := os.Open(path)
if err != nil {
return values
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.ToUpper(strings.TrimSpace(parts[0]))
value := strings.Trim(strings.TrimSpace(parts[1]), "\"")
values[key] = strings.ToLower(value)
}
return values
}
func hasAnyToken(text string, tokens ...string) bool {
if text == "" {
return false
}
for _, token := range tokens {
if strings.Contains(text, token) {
return true
}
}
return false
}
func detectImmutableSystem() (bool, string) {
if _, err := os.Stat("/run/ostree-booted"); err == nil {
return true, "/run/ostree-booted is present"
}
osRelease := readOSReleaseMap("/etc/os-release")
if len(osRelease) == 0 {
return false, ""
}
id := osRelease["ID"]
idLike := osRelease["ID_LIKE"]
variantID := osRelease["VARIANT_ID"]
name := osRelease["NAME"]
prettyName := osRelease["PRETTY_NAME"]
immutableIDs := map[string]bool{
"bluefin": true,
"bazzite": true,
"silverblue": true,
"kinoite": true,
"sericea": true,
"onyx": true,
"aurora": true,
"fedora-iot": true,
"fedora-coreos": true,
}
if immutableIDs[id] {
return true, "os-release ID=" + id
}
markers := []string{"silverblue", "kinoite", "sericea", "onyx", "bazzite", "bluefin", "aurora", "ostree", "atomic"}
if hasAnyToken(variantID, markers...) {
return true, "os-release VARIANT_ID=" + variantID
}
if hasAnyToken(idLike, "ostree", "rpm-ostree") {
return true, "os-release ID_LIKE=" + idLike
}
if hasAnyToken(name, markers...) || hasAnyToken(prettyName, markers...) {
return true, "os-release identifies an atomic/ostree variant"
}
return false, ""
}
func getImmutablePolicy() (*immutableCommandPolicy, error) {
immutablePolicyOnce.Do(func() {
detectedImmutable, reason := detectImmutableSystem()
immutablePolicy = immutableCommandPolicy{
ImmutableSystem: detectedImmutable,
ImmutableReason: reason,
BlockedCommands: []string{"greeter install", "greeter enable", "setup"},
Message: "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes.",
}
var defaultPolicy cliPolicyFile
if err := json.Unmarshal(defaultCLIPolicyJSON, &defaultPolicy); err != nil {
immutablePolicyErr = fmt.Errorf("failed to parse embedded default CLI policy: %w", err)
return
}
if defaultPolicy.BlockedCommands != nil {
immutablePolicy.BlockedCommands = normalizeBlockedCommands(*defaultPolicy.BlockedCommands)
}
if defaultPolicy.Message != nil {
msg := strings.TrimSpace(*defaultPolicy.Message)
if msg != "" {
immutablePolicy.Message = msg
}
}
if err := mergePolicyFile(&immutablePolicy, cliPolicyPackagedPath); err != nil {
immutablePolicyErr = err
return
}
if err := mergePolicyFile(&immutablePolicy, cliPolicyAdminPath); err != nil {
immutablePolicyErr = err
return
}
})
if immutablePolicyErr != nil {
return nil, immutablePolicyErr
}
return &immutablePolicy, nil
}
func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
policy, err := getImmutablePolicy()
if err != nil {
return err
}
if !policy.ImmutableSystem {
return nil
}
commandPath := normalizeCommandSpec(cmd.CommandPath())
if !commandBlockedByPolicy(commandPath, policy.BlockedCommands) {
return nil
}
reason := ""
if policy.ImmutableReason != "" {
reason = "Detected immutable system: " + policy.ImmutableReason + "\n"
}
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
}
+10 -1
View File
@@ -16,10 +16,19 @@ func init() {
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)") runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().MarkHidden("daemon-child") runCmd.Flags().MarkHidden("daemon-child")
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd) // Add subcommands to greeter
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to setup
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
// Add subcommands to update
updateCmd.AddCommand(updateCheckCmd) updateCmd.AddCommand(updateCheckCmd)
// Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)
rootCmd.AddCommand(updateCmd) rootCmd.AddCommand(updateCmd)
+10 -1
View File
@@ -11,20 +11,29 @@ import (
var Version = "dev" var Version = "dev"
func init() { func init() {
// Add flags
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode") runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process") runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)") runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().MarkHidden("daemon-child") runCmd.Flags().MarkHidden("daemon-child")
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd) // Add subcommands to greeter
greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to setup
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
// Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)
rootCmd.SetHelpTemplate(getHelpTemplate()) rootCmd.SetHelpTemplate(getHelpTemplate())
} }
func main() { func main() {
// Block root
if os.Geteuid() == 0 { if os.Geteuid() == 0 {
log.Fatal("This program should not be run as root. Exiting.") log.Fatal("This program should not be run as root. Exiting.")
} }
-172
View File
@@ -1,172 +0,0 @@
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
}
+2 -2
View File
@@ -210,7 +210,7 @@ func runShellInteractive(session bool) {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3") cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
} }
if os.Getenv("QT_QPA_PLATFORM") == "" { if os.Getenv("QT_QPA_PLATFORM") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb") cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland")
} }
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
@@ -450,7 +450,7 @@ func runShellDaemon(session bool) {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3") cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
} }
if os.Getenv("QT_QPA_PLATFORM") == "" { if os.Getenv("QT_QPA_PLATFORM") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb") cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland")
} }
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0) devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
+8
View File
@@ -7,6 +7,14 @@ import (
"strings" "strings"
) )
func findCommandPath(cmd string) (string, error) {
path, err := exec.LookPath(cmd)
if err != nil {
return "", fmt.Errorf("command '%s' not found in PATH", cmd)
}
return path, nil
}
func isArchPackageInstalled(packageName string) bool { func isArchPackageInstalled(packageName string) bool {
cmd := exec.Command("pacman", "-Q", packageName) cmd := exec.Command("pacman", "-Q", packageName)
err := cmd.Run() err := cmd.Run()
+5 -5
View File
@@ -19,7 +19,7 @@ require (
github.com/yuin/goldmark v1.7.16 github.com/yuin/goldmark v1.7.16
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.etcd.io/bbolt v1.4.3 go.etcd.io/bbolt v1.4.3
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a golang.org/x/exp v0.0.0-20260211191109-2735e65f0518
golang.org/x/image v0.36.0 golang.org/x/image v0.36.0
) )
@@ -27,7 +27,7 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/clipperhouse/displaywidth v0.10.0 // indirect github.com/clipperhouse/displaywidth v0.10.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/clipperhouse/uax29/v2 v2.6.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect github.com/cloudflare/circl v1.6.3 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
@@ -36,7 +36,7 @@ require (
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 // indirect github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/kevinburke/ssh_config v1.6.0 // indirect github.com/kevinburke/ssh_config v1.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect
@@ -55,7 +55,7 @@ require (
github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f github.com/go-git/go-git/v6 v6.0.0-20260210102253-e4d10f0e569a
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 github.com/lucasb-eyer/go-colorful v1.3.0
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
@@ -71,7 +71,7 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.41.0 golang.org/x/sys v0.41.0
golang.org/x/text v0.34.0 golang.org/x/text v0.34.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1 // indirect
) )
// v0.0.1 tag is missing a LICENSE file; master has it. // v0.0.1 tag is missing a LICENSE file; master has it.
+8 -8
View File
@@ -40,8 +40,8 @@ github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSg
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g= github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs= github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -68,8 +68,8 @@ github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 h1:UU7oARtwQ5g8
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3/go.mod h1:ZW9JC5gionMP1kv5uiaOaV23q0FFmNrVOV8VW+y/acc= github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3/go.mod h1:ZW9JC5gionMP1kv5uiaOaV23q0FFmNrVOV8VW+y/acc=
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67 h1:3hutPZF+/FBjR/9MdsLJ7e1mlt9pwHgwxMW7CrbmWII= github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67 h1:3hutPZF+/FBjR/9MdsLJ7e1mlt9pwHgwxMW7CrbmWII=
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67/go.mod h1:xKt0pNHST9tYHvbiLxSY27CQWFwgIxBJuDrOE0JvbZw= github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67/go.mod h1:xKt0pNHST9tYHvbiLxSY27CQWFwgIxBJuDrOE0JvbZw=
github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f h1:TBkCJv9YwPOuXq1OG0r01bcxRrvs15Hp/DtZuPt4H6s= github.com/go-git/go-git/v6 v6.0.0-20260210102253-e4d10f0e569a h1:LLju0NuXQqR4WmGl1Dm86b9ZXsvXgLYbx/aaAjdQr6w=
github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f/go.mod h1:B88nWzfnhTlIikoJ4d84Nc9noKS5mJoA7SgDdkt0aPU= github.com/go-git/go-git/v6 v6.0.0-20260210102253-e4d10f0e569a/go.mod h1:IdXOePSwsMKGpuAbpczsm+f0Uy5fdHHjwgDPOymKA78=
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE= github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk= github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -86,8 +86,8 @@ github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 h1:B+A58zGFuDrvE
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk= github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY= github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -152,8 +152,8 @@ go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o= golang.org/x/exp v0.0.0-20260211191109-2735e65f0518 h1:2E1CW7v5QB+Wi3N+MXllOtVR6SFmI8iJM8EdzgxrgrU=
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/exp v0.0.0-20260211191109-2735e65f0518/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc= golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4= golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
+8 -92
View File
@@ -1,12 +1,10 @@
package clipboard package clipboard
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"syscall" "syscall"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
@@ -14,37 +12,17 @@ import (
) )
func Copy(data []byte, mimeType string) error { func Copy(data []byte, mimeType string) error {
return CopyReader(bytes.NewReader(data), mimeType, false, false) return CopyOpts(data, mimeType, false, false)
} }
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error { func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
if foreground {
return copyServeWithWriter(func(writer io.Writer) error {
total := 0
for total < len(data) {
n, err := writer.Write(data[total:])
total += n
if err != nil {
return err
}
}
if total != len(data) {
return io.ErrShortWrite
}
return nil
}, mimeType, pasteOnce)
}
return CopyReader(bytes.NewReader(data), mimeType, foreground, pasteOnce)
}
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
if !foreground { if !foreground {
return copyFork(data, mimeType, pasteOnce) return copyFork(data, mimeType, pasteOnce)
} }
return copyServeReader(data, mimeType, pasteOnce) return copyServe(data, mimeType, pasteOnce)
} }
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error { func copyFork(data []byte, mimeType string, pasteOnce bool) error {
args := []string{os.Args[0], "cl", "copy", "--foreground"} args := []string{os.Args[0], "cl", "copy", "--foreground"}
if pasteOnce { if pasteOnce {
args = append(args, "--paste-once") args = append(args, "--paste-once")
@@ -52,15 +30,11 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
args = append(args, "--type", mimeType) args = append(args, "--type", mimeType)
cmd := exec.Command(args[0], args[1:]...) cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = nil
cmd.Stdout = nil cmd.Stdout = nil
cmd.Stderr = nil cmd.Stderr = nil
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
if stdinSource, ok := data.(*os.File); ok {
cmd.Stdin = stdinSource
return cmd.Start()
}
stdin, err := cmd.StdinPipe() stdin, err := cmd.StdinPipe()
if err != nil { if err != nil {
return fmt.Errorf("stdin pipe: %w", err) return fmt.Errorf("stdin pipe: %w", err)
@@ -70,66 +44,16 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
return fmt.Errorf("start: %w", err) return fmt.Errorf("start: %w", err)
} }
if _, err := io.Copy(stdin, data); err != nil { if _, err := stdin.Write(data); err != nil {
stdin.Close() stdin.Close()
return fmt.Errorf("write stdin: %w", err) return fmt.Errorf("write stdin: %w", err)
} }
if err := stdin.Close(); err != nil { stdin.Close()
return fmt.Errorf("close stdin: %w", err)
}
return nil return nil
} }
func copyServeReader(data io.Reader, mimeType string, pasteOnce bool) error { func copyServe(data []byte, mimeType string, pasteOnce bool) error {
cachedData, err := createClipboardCacheFile()
if err != nil {
return fmt.Errorf("create clipboard cache file: %w", err)
}
defer os.Remove(cachedData.Name())
if _, err := io.Copy(cachedData, data); err != nil {
return fmt.Errorf("cache clipboard data: %w", err)
}
if err := cachedData.Close(); err != nil {
return fmt.Errorf("close temp cache file: %w", err)
}
return copyServeWithWriter(func(writer io.Writer) error {
cachedFile, err := os.Open(cachedData.Name())
if err != nil {
return fmt.Errorf("open temp cache file: %w", err)
}
defer cachedFile.Close()
if _, err := io.Copy(writer, cachedFile); err != nil {
return fmt.Errorf("write clipboard data: %w", err)
}
return nil
}, mimeType, pasteOnce)
}
func createClipboardCacheFile() (*os.File, error) {
preferredDirs := []string{}
if cacheDir, err := os.UserCacheDir(); err == nil {
preferredDirs = append(preferredDirs, filepath.Join(cacheDir, "dms", "clipboard"))
}
preferredDirs = append(preferredDirs, "/var/tmp/dms/clipboard")
for _, dir := range preferredDirs {
if err := os.MkdirAll(dir, 0o700); err != nil {
continue
}
cachedData, err := os.CreateTemp(dir, "dms-clipboard-*")
if err == nil {
return cachedData, nil
}
}
return os.CreateTemp("", "dms-clipboard-*")
}
func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOnce bool) error {
display, err := wlclient.Connect("") display, err := wlclient.Connect("")
if err != nil { if err != nil {
return fmt.Errorf("wayland connect: %w", err) return fmt.Errorf("wayland connect: %w", err)
@@ -215,18 +139,12 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
cancelled := make(chan struct{}) cancelled := make(chan struct{})
pasted := make(chan struct{}, 1) pasted := make(chan struct{}, 1)
sendErr := make(chan error, 1)
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) { source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
defer syscall.Close(e.Fd) defer syscall.Close(e.Fd)
file := os.NewFile(uintptr(e.Fd), "pipe") file := os.NewFile(uintptr(e.Fd), "pipe")
defer file.Close() defer file.Close()
if err := writeTo(file); err != nil { file.Write(data)
select {
case sendErr <- err:
default:
}
}
select { select {
case pasted <- struct{}{}: case pasted <- struct{}{}:
default: default:
@@ -247,8 +165,6 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
select { select {
case <-cancelled: case <-cancelled:
return nil return nil
case err := <-sendErr:
return err
case <-pasted: case <-pasted:
if pasteOnce { if pasteOnce {
return nil return nil
+3 -3
View File
@@ -644,7 +644,7 @@ func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalC
if strings.HasPrefix(trimmed, "exec-once = systemctl --user start") { if strings.HasPrefix(trimmed, "exec-once = systemctl --user start") {
startupSectionFound = true startupSectionFound = true
result = append(result, "exec-once = dms run") result = append(result, "exec-once = dms run")
result = append(result, "env = QT_QPA_PLATFORM,wayland;xcb") result = append(result, "env = QT_QPA_PLATFORM,wayland")
result = append(result, "env = ELECTRON_OZONE_PLATFORM_HINT,auto") result = append(result, "env = ELECTRON_OZONE_PLATFORM_HINT,auto")
result = append(result, "env = QT_QPA_PLATFORMTHEME,gtk3") result = append(result, "env = QT_QPA_PLATFORMTHEME,gtk3")
result = append(result, "env = QT_QPA_PLATFORMTHEME_QT6,gtk3") result = append(result, "env = QT_QPA_PLATFORMTHEME_QT6,gtk3")
@@ -659,7 +659,7 @@ func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalC
if strings.Contains(line, "STARTUP APPS") { if strings.Contains(line, "STARTUP APPS") {
insertLines := []string{ insertLines := []string{
"exec-once = dms run", "exec-once = dms run",
"env = QT_QPA_PLATFORM,wayland;xcb", "env = QT_QPA_PLATFORM,wayland",
"env = ELECTRON_OZONE_PLATFORM_HINT,auto", "env = ELECTRON_OZONE_PLATFORM_HINT,auto",
"env = QT_QPA_PLATFORMTHEME,gtk3", "env = QT_QPA_PLATFORMTHEME,gtk3",
"env = QT_QPA_PLATFORMTHEME_QT6,gtk3", "env = QT_QPA_PLATFORMTHEME_QT6,gtk3",
@@ -677,7 +677,7 @@ func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalC
func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalCommand string) string { func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalCommand string) string {
envVars := fmt.Sprintf(`environment { envVars := fmt.Sprintf(`environment {
XDG_CURRENT_DESKTOP "niri" XDG_CURRENT_DESKTOP "niri"
QT_QPA_PLATFORM "wayland;xcb" QT_QPA_PLATFORM "wayland"
ELECTRON_OZONE_PLATFORM_HINT "auto" ELECTRON_OZONE_PLATFORM_HINT "auto"
QT_QPA_PLATFORMTHEME "gtk3" QT_QPA_PLATFORMTHEME "gtk3"
QT_QPA_PLATFORMTHEME_QT6 "gtk3" QT_QPA_PLATFORMTHEME_QT6 "gtk3"
+1 -2
View File
@@ -100,7 +100,7 @@ windowrule = float on, match:class ^(blueman-manager)$
windowrule = float on, match:class ^(org\.gnome\.Nautilus)$ windowrule = float on, match:class ^(org\.gnome\.Nautilus)$
windowrule = float on, match:class ^(xdg-desktop-portal)$ windowrule = float on, match:class ^(xdg-desktop-portal)$
windowrule = no_initial_focus on, match:class ^(steam)$, match:title ^(notificationtoasts) windowrule = noinitialfocus on, match:class ^(steam)$, match:title ^(notificationtoasts)
windowrule = pin on, match:class ^(steam)$, match:title ^(notificationtoasts) windowrule = pin on, match:class ^(steam)$, match:title ^(notificationtoasts)
windowrule = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture)$ windowrule = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture)$
@@ -111,7 +111,6 @@ windowrule = float on, match:class ^(zoom)$
# windowrule = float on, match:class ^(org.quickshell)$ # windowrule = float on, match:class ^(org.quickshell)$
layerrule = no_anim on, match:namespace ^(quickshell)$ layerrule = no_anim on, match:namespace ^(quickshell)$
layerrule = no_anim on, match:namespace ^dms:.*
source = ./dms/colors.conf source = ./dms/colors.conf
source = ./dms/outputs.conf source = ./dms/outputs.conf
-1
View File
@@ -252,7 +252,6 @@ window-rule {
// Open dms windows as floating by default // Open dms windows as floating by default
window-rule { window-rule {
match app-id=r#"org.quickshell$"# match app-id=r#"org.quickshell$"#
match app-id=r#"com.danklinux.dms$"#
open-floating true open-floating true
} }
debug { debug {
+62 -118
View File
@@ -26,9 +26,6 @@ func init() {
Register("cachyos", "#08A283", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution { Register("cachyos", "#08A283", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan) return NewArchDistribution(config, logChan)
}) })
Register("catos", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
Register("endeavouros", "#7F3FBF", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution { Register("endeavouros", "#7F3FBF", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan) return NewArchDistribution(config, logChan)
}) })
@@ -97,7 +94,6 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
dependencies = append(dependencies, a.detectGit()) dependencies = append(dependencies, a.detectGit())
dependencies = append(dependencies, a.detectWindowManager(wm)) dependencies = append(dependencies, a.detectWindowManager(wm))
dependencies = append(dependencies, a.detectQuickshell()) dependencies = append(dependencies, a.detectQuickshell())
dependencies = append(dependencies, a.detectDMSGreeter())
dependencies = append(dependencies, a.detectXDGPortal()) dependencies = append(dependencies, a.detectXDGPortal())
dependencies = append(dependencies, a.detectAccountsService()) dependencies = append(dependencies, a.detectAccountsService())
@@ -125,52 +121,12 @@ func (a *ArchDistribution) detectAccountsService() deps.Dependency {
return a.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", a.packageInstalled("accountsservice")) return a.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", a.packageInstalled("accountsservice"))
} }
func (a *ArchDistribution) detectDMSGreeter() deps.Dependency {
return a.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", a.packageInstalled("greetd-dms-greeter-git"))
}
func (a *ArchDistribution) packageInstalled(pkg string) bool { func (a *ArchDistribution) packageInstalled(pkg string) bool {
cmd := exec.Command("pacman", "-Q", pkg) cmd := exec.Command("pacman", "-Q", pkg)
err := cmd.Run() err := cmd.Run()
return err == nil return err == nil
} }
// parseSRCINFODeps reads a .SRCINFO file and returns runtime dep and makedep package
func parseSRCINFODeps(srcinfoPath string) (deps []string, makedeps []string, err error) {
data, err := os.ReadFile(srcinfoPath)
if err != nil {
return nil, nil, err
}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
var pkg string
var target *[]string
switch {
case strings.HasPrefix(line, "makedepends = "):
pkg = strings.TrimPrefix(line, "makedepends = ")
target = &makedeps
case strings.HasPrefix(line, "depends = "):
pkg = strings.TrimPrefix(line, "depends = ")
target = &deps
default:
continue
}
// Strip version constraint (>=, <=, >, <, =) and colon-descriptions
if idx := strings.IndexAny(pkg, "><:="); idx >= 0 {
pkg = pkg[:idx]
}
pkg = strings.TrimSpace(pkg)
if pkg != "" {
*target = append(*target, pkg)
}
}
return deps, makedeps, nil
}
func (a *ArchDistribution) isInSystemRepo(pkg string) bool {
return exec.Command("pacman", "-Si", pkg).Run() == nil
}
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant)) return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
} }
@@ -180,7 +136,6 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
"dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]), "dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]),
"git": {Name: "git", Repository: RepoTypeSystem}, "git": {Name: "git", Repository: RepoTypeSystem},
"quickshell": a.getQuickshellMapping(variants["quickshell"]), "quickshell": a.getQuickshellMapping(variants["quickshell"]),
"dms-greeter": {Name: "greetd-dms-greeter-git", Repository: RepoTypeAUR},
"matugen": a.getMatugenMapping(variants["matugen"]), "matugen": a.getMatugenMapping(variants["matugen"]),
"dgop": {Name: "dgop", Repository: RepoTypeSystem}, "dgop": {Name: "dgop", Repository: RepoTypeSystem},
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem}, "ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
@@ -476,10 +431,29 @@ func (a *ArchDistribution) installAURPackages(ctx context.Context, packages []st
a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", "))) a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", ")))
hasNiri := false hasNiri := false
hasQuickshell := false
for _, pkg := range packages { for _, pkg := range packages {
if pkg == "niri-git" { if pkg == "niri-git" {
hasNiri = true hasNiri = true
} }
if pkg == "quickshell" || pkg == "quickshell-git" {
hasQuickshell = true
}
}
// If quickshell is in the list, always reinstall google-breakpad first
if hasQuickshell {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.63,
Step: "Reinstalling google-breakpad for quickshell...",
IsComplete: false,
CommandInfo: "Reinstalling prerequisite AUR package for quickshell",
}
if err := a.installSingleAURPackage(ctx, "google-breakpad", sudoPassword, progressChan, 0.63, 0.65); err != nil {
return fmt.Errorf("failed to reinstall google-breakpad prerequisite for quickshell: %w", err)
}
} }
// If niri is in the list, install makepkg-git-lfs-proto first if not already installed // If niri is in the list, install makepkg-git-lfs-proto first if not already installed
@@ -560,16 +534,6 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
} }
func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error { func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error {
return a.installSingleAURPackageInternal(ctx, pkg, sudoPassword, progressChan, startProgress, endProgress, make(map[string]bool))
}
func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64, visited map[string]bool) error {
if visited[pkg] {
a.log(fmt.Sprintf("Skipping %s (already being installed, cycle detected)", pkg))
return nil
}
visited[pkg] = true
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err) return fmt.Errorf("failed to get user home directory: %w", err)
@@ -643,8 +607,48 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err) return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err)
} }
srcinfoPath = filepath.Join(packageDir, ".SRCINFO") // Skip dependency installation for dms-shell-git and dms-shell-bin
if pkg == "dms-shell-bin" { // since we manually manage those dependencies
if pkg != "dms-shell-git" && pkg != "dms-shell-bin" {
// Pre-install dependencies from .SRCINFO
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.3*(endProgress-startProgress),
Step: fmt.Sprintf("Installing dependencies for %s...", pkg),
IsComplete: false,
CommandInfo: "Installing package dependencies and makedepends",
}
// Install dependencies and makedepends explicitly
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
depsCmd := exec.CommandContext(ctx, "bash", "-c",
fmt.Sprintf(`
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
if [[ "%s" == *"quickshell"* ]]; then
deps=$(echo "$deps" | sed 's/google-breakpad//g' | sed 's/ / /g' | sed 's/^ *//g' | sed 's/ *$//g')
fi
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
fi
`, srcinfoPath, pkg, sudoPassword))
if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil {
return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err)
}
makedepsCmd := exec.CommandContext(ctx, "bash", "-c",
fmt.Sprintf(`
makedeps=$(grep -E "^[[:space:]]*makedepends = " "%s" | sed 's/^[[:space:]]*makedepends = //' | tr '\n' ' ')
if [ ! -z "$makedeps" ]; then
echo '%s' | sudo -S pacman -S --needed --noconfirm $makedeps
fi
`, srcinfoPath, sudoPassword))
if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil {
return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err)
}
} else {
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages, Phase: PhaseAURPackages,
Progress: startProgress + 0.35*(endProgress-startProgress), Progress: startProgress + 0.35*(endProgress-startProgress),
@@ -652,66 +656,6 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
IsComplete: false, IsComplete: false,
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg), LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
} }
} else {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.3*(endProgress-startProgress),
Step: fmt.Sprintf("Resolving dependencies for %s...", pkg),
IsComplete: false,
CommandInfo: "Classifying dependencies as system or AUR",
}
runtimeDeps, makeDeps, err := parseSRCINFODeps(srcinfoPath)
if err != nil {
return fmt.Errorf("failed to parse .SRCINFO for %s: %w", pkg, err)
}
seen := make(map[string]bool)
var systemPkgs []string
var aurPkgs []string
for _, dep := range append(runtimeDeps, makeDeps...) {
if seen[dep] || a.packageInstalled(dep) {
continue
}
seen[dep] = true
if a.isInSystemRepo(dep) {
systemPkgs = append(systemPkgs, dep)
} else {
aurPkgs = append(aurPkgs, dep)
}
}
if len(systemPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.32*(endProgress-startProgress),
Step: fmt.Sprintf("Installing %d system dependencies for %s...", len(systemPkgs), pkg),
IsComplete: false,
CommandInfo: fmt.Sprintf("sudo pacman -S --needed --noconfirm %s", strings.Join(systemPkgs, " ")),
}
if err := a.installSystemPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install system dependencies for %s: %w", pkg, err)
}
}
for _, aurDep := range aurPkgs {
a.log(fmt.Sprintf("Dependency %s is AUR-only, building from source...", aurDep))
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.35*(endProgress-startProgress),
Step: fmt.Sprintf("Installing AUR dependency %s for %s...", aurDep, pkg),
IsComplete: false,
CommandInfo: fmt.Sprintf("Building AUR dependency: %s", aurDep),
}
if err := a.installSingleAURPackageInternal(ctx, aurDep, sudoPassword, progressChan,
startProgress+0.35*(endProgress-startProgress),
startProgress+0.39*(endProgress-startProgress),
visited,
); err != nil {
return fmt.Errorf("failed to install AUR dependency %s for %s: %w", aurDep, pkg, err)
}
}
} }
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
@@ -724,7 +668,7 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm") buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
buildCmd.Dir = packageDir buildCmd.Dir = packageDir
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar") buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar") // Disable compression for speed
if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil { if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil {
return fmt.Errorf("failed to build %s: %w", pkg, err) return fmt.Errorf("failed to build %s: %w", pkg, err)
-13
View File
@@ -102,19 +102,6 @@ func (b *BaseDistribution) detectPackage(name, description string, installed boo
} }
} }
func (b *BaseDistribution) detectOptionalPackage(name, description string, installed bool) deps.Dependency {
status := deps.StatusMissing
if installed {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: name,
Status: status,
Description: description,
Required: false,
}
}
func (b *BaseDistribution) detectGit() deps.Dependency { func (b *BaseDistribution) detectGit() deps.Dependency {
return b.detectCommand("git", "Version control system") return b.detectCommand("git", "Version control system")
} }
+2 -18
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"os/exec" "os/exec"
"runtime"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
@@ -60,7 +61,6 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, d.detectGit()) dependencies = append(dependencies, d.detectGit())
dependencies = append(dependencies, d.detectWindowManager(wm)) dependencies = append(dependencies, d.detectWindowManager(wm))
dependencies = append(dependencies, d.detectQuickshell()) dependencies = append(dependencies, d.detectQuickshell())
dependencies = append(dependencies, d.detectDMSGreeter())
dependencies = append(dependencies, d.detectXDGPortal()) dependencies = append(dependencies, d.detectXDGPortal())
dependencies = append(dependencies, d.detectAccountsService()) dependencies = append(dependencies, d.detectAccountsService())
@@ -86,27 +86,12 @@ func (d *DebianDistribution) detectAccountsService() deps.Dependency {
return d.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", d.packageInstalled("accountsservice")) return d.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", d.packageInstalled("accountsservice"))
} }
func (d *DebianDistribution) detectDMSGreeter() deps.Dependency {
return d.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", d.packageInstalled("dms-greeter"))
}
func (d *DebianDistribution) packageInstalled(pkg string) bool { func (d *DebianDistribution) packageInstalled(pkg string) bool {
cmd := exec.Command("dpkg", "-l", pkg) cmd := exec.Command("dpkg", "-l", pkg)
err := cmd.Run() err := cmd.Run()
return err == nil return err == nil
} }
func debianRepoArchitecture(arch string) string {
switch arch {
case "amd64", "x86_64":
return "amd64"
case "arm64", "aarch64":
return "arm64"
default:
return arch
}
}
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return d.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant)) return d.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
} }
@@ -123,7 +108,6 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
// DMS packages from OBS with variant support // DMS packages from OBS with variant support
"dms (DankMaterialShell)": d.getDmsMapping(variants["dms (DankMaterialShell)"]), "dms (DankMaterialShell)": d.getDmsMapping(variants["dms (DankMaterialShell)"]),
"quickshell": d.getQuickshellMapping(variants["quickshell"]), "quickshell": d.getQuickshellMapping(variants["quickshell"]),
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
@@ -446,7 +430,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
} }
// Add repository // Add repository
repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, debianRepoArchitecture(osInfo.Architecture), baseURL) repoLine := fmt.Sprintf("deb [signed-by=%s, arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages, Phase: PhaseSystemPackages,
-9
View File
@@ -13,9 +13,6 @@ func init() {
Register("fedora", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution { Register("fedora", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan) return NewFedoraDistribution(config, logChan)
}) })
Register("evernight", "#72B8DC", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
Register("nobara", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution { Register("nobara", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan) return NewFedoraDistribution(config, logChan)
}) })
@@ -78,7 +75,6 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, f.detectGit()) dependencies = append(dependencies, f.detectGit())
dependencies = append(dependencies, f.detectWindowManager(wm)) dependencies = append(dependencies, f.detectWindowManager(wm))
dependencies = append(dependencies, f.detectQuickshell()) dependencies = append(dependencies, f.detectQuickshell())
dependencies = append(dependencies, f.detectDMSGreeter())
dependencies = append(dependencies, f.detectXDGPortal()) dependencies = append(dependencies, f.detectXDGPortal())
dependencies = append(dependencies, f.detectAccountsService()) dependencies = append(dependencies, f.detectAccountsService())
@@ -124,7 +120,6 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
// COPR packages // COPR packages
"quickshell": f.getQuickshellMapping(variants["quickshell"]), "quickshell": f.getQuickshellMapping(variants["quickshell"]),
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}, "matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]), "dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
"dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}, "dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
@@ -196,10 +191,6 @@ func (f *FedoraDistribution) detectAccountsService() deps.Dependency {
} }
} }
func (f *FedoraDistribution) detectDMSGreeter() deps.Dependency {
return f.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", f.packageInstalled("dms-greeter"))
}
func (f *FedoraDistribution) getPrerequisites() []string { func (f *FedoraDistribution) getPrerequisites() []string {
return []string{ return []string{
"dnf-plugins-core", "dnf-plugins-core",
-1
View File
@@ -55,7 +55,6 @@ const (
PhaseAURPackages PhaseAURPackages
PhaseCursorTheme PhaseCursorTheme
PhaseConfiguration PhaseConfiguration
PhaseGreeterSetup
PhaseComplete PhaseComplete
) )
-6
View File
@@ -71,7 +71,6 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
dependencies = append(dependencies, o.detectGit()) dependencies = append(dependencies, o.detectGit())
dependencies = append(dependencies, o.detectWindowManager(wm)) dependencies = append(dependencies, o.detectWindowManager(wm))
dependencies = append(dependencies, o.detectQuickshell()) dependencies = append(dependencies, o.detectQuickshell())
dependencies = append(dependencies, o.detectDMSGreeter())
dependencies = append(dependencies, o.detectXDGPortal()) dependencies = append(dependencies, o.detectXDGPortal())
dependencies = append(dependencies, o.detectAccountsService()) dependencies = append(dependencies, o.detectAccountsService())
@@ -101,10 +100,6 @@ func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {
return err == nil return err == nil
} }
func (o *OpenSUSEDistribution) detectDMSGreeter() deps.Dependency {
return o.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", o.packageInstalled("dms-greeter"))
}
func (o *OpenSUSEDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { func (o *OpenSUSEDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return o.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant)) return o.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
} }
@@ -121,7 +116,6 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
// DMS packages from OBS // DMS packages from OBS
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]), "dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
"quickshell": o.getQuickshellMapping(variants["quickshell"]), "quickshell": o.getQuickshellMapping(variants["quickshell"]),
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
-6
View File
@@ -63,7 +63,6 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, u.detectGit()) dependencies = append(dependencies, u.detectGit())
dependencies = append(dependencies, u.detectWindowManager(wm)) dependencies = append(dependencies, u.detectWindowManager(wm))
dependencies = append(dependencies, u.detectQuickshell()) dependencies = append(dependencies, u.detectQuickshell())
dependencies = append(dependencies, u.detectDMSGreeter())
dependencies = append(dependencies, u.detectXDGPortal()) dependencies = append(dependencies, u.detectXDGPortal())
dependencies = append(dependencies, u.detectAccountsService()) dependencies = append(dependencies, u.detectAccountsService())
@@ -95,10 +94,6 @@ func (u *UbuntuDistribution) detectAccountsService() deps.Dependency {
return u.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", u.packageInstalled("accountsservice")) return u.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", u.packageInstalled("accountsservice"))
} }
func (u *UbuntuDistribution) detectDMSGreeter() deps.Dependency {
return u.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", u.packageInstalled("dms-greeter"))
}
func (u *UbuntuDistribution) packageInstalled(pkg string) bool { func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
cmd := exec.Command("dpkg", "-l", pkg) cmd := exec.Command("dpkg", "-l", pkg)
err := cmd.Run() err := cmd.Run()
@@ -121,7 +116,6 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
// DMS packages from PPAs // DMS packages from PPAs
"dms (DankMaterialShell)": u.getDmsMapping(variants["dms (DankMaterialShell)"]), "dms (DankMaterialShell)": u.getDmsMapping(variants["dms (DankMaterialShell)"]),
"quickshell": u.getQuickshellMapping(variants["quickshell"]), "quickshell": u.getQuickshellMapping(variants["quickshell"]),
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}, "matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}, "dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}, "ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
@@ -1,91 +0,0 @@
# AppArmor profile for dms-greeter
#
# Managed by DMS — regenerated on every `dms greeter install` / `dms greeter sync`.
# Manual edits will be overwritten on next sync.
#
# Mode: complain (denials are logged, nothing is blocked)
# To switch to enforce after validating with `aa-logprof`:
# sudo aa-enforce /etc/apparmor.d/usr.bin.dms-greeter
#
#include <tunables/global>
profile dms-greeter /usr/bin/dms-greeter flags=(complain) {
#include <abstractions/base>
#include <abstractions/bash>
# The launcher script itself
/usr/bin/dms-greeter r,
# Cache directory — created by dms greeter sync/enable with greeter:greeter ownership
/var/cache/dms-greeter/ rw,
/var/cache/dms-greeter/** rwlk,
# DMS config — packaged path
/usr/share/quickshell/dms-greeter/ r,
/usr/share/quickshell/dms-greeter/** r,
/usr/share/quickshell/ r,
/usr/share/quickshell/** r,
# DMS config — system and user overrides
/etc/dms/ r,
/etc/dms/** r,
/usr/share/dms/ r,
/usr/share/dms/** r,
/home/*/.config/quickshell/ r,
/home/*/.config/quickshell/** r,
/root/.config/quickshell/ r,
/root/.config/quickshell/** r,
# greetd / PAM — read-only for session setup
/etc/greetd/ r,
/etc/greetd/** r,
/etc/pam.d/ r,
/etc/pam.d/** r,
/usr/lib/pam.d/ r,
/usr/lib/pam.d/** r,
# Compositor binaries — run unconfined so each compositor uses its own profile
/usr/bin/niri Ux,
/usr/bin/hyprland Ux,
/usr/bin/Hyprland Ux,
/usr/bin/sway Ux,
/usr/bin/labwc Ux,
/usr/bin/scroll Ux,
/usr/bin/miracle-wm Ux,
/usr/bin/mango Ux,
# Quickshell — run unconfined (has its own compositor profile on some distros)
/usr/bin/qs Ux,
/usr/bin/quickshell Ux,
# Wayland / XDG runtime (pipewire, wireplumber, wayland socket)
/run/user/[0-9]*/ rw,
/run/user/[0-9]*/** rw,
# DRM / GPU devices (required for Wayland compositor startup)
/dev/dri/ r,
/dev/dri/* rw,
/dev/udmabuf rw,
# Input devices
/dev/input/ r,
/dev/input/* r,
# Systemd journal / logging
/run/systemd/journal/socket rw,
/dev/log rw,
# Shell helper binaries invoked by the launcher script
/usr/bin/env ix,
/usr/bin/mkdir ix,
/usr/bin/cat ix,
/usr/bin/grep ix,
/usr/bin/dirname ix,
/usr/bin/basename ix,
/usr/bin/command ix,
/bin/env ix,
/bin/mkdir ix,
# Signal management (compositor lifecycle)
signal (send, receive) set=("term", "int", "hup", "kill"),
}
File diff suppressed because it is too large Load Diff
-98
View File
@@ -1,98 +0,0 @@
package greeter
import (
"os"
"path/filepath"
"testing"
)
func writeTestJSON(t *testing.T, path string, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("failed to create parent dir for %s: %v", path, err)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatalf("failed to write %s: %v", path, err)
}
}
func TestResolveGreeterThemeSyncState(t *testing.T) {
t.Parallel()
tests := []struct {
name string
settingsJSON string
sessionJSON string
wantSourcePath string
wantResolvedWallpaper string
wantDynamicOverrideUsed bool
}{
{
name: "dynamic theme with greeter wallpaper override uses generated greeter colors",
settingsJSON: `{
"currentThemeName": "dynamic",
"greeterWallpaperPath": "Pictures/blue.jpg",
"matugenScheme": "scheme-tonal-spot",
"iconTheme": "Papirus"
}`,
sessionJSON: `{"isLightMode":true}`,
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "greeter-colors", "dms-colors.json"),
wantResolvedWallpaper: filepath.Join("Pictures", "blue.jpg"),
wantDynamicOverrideUsed: true,
},
{
name: "dynamic theme without override uses desktop colors",
settingsJSON: `{
"currentThemeName": "dynamic",
"greeterWallpaperPath": ""
}`,
sessionJSON: `{"isLightMode":false}`,
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
wantResolvedWallpaper: "",
wantDynamicOverrideUsed: false,
},
{
name: "non-dynamic theme keeps desktop colors even with override wallpaper",
settingsJSON: `{
"currentThemeName": "purple",
"greeterWallpaperPath": "/tmp/blue.jpg"
}`,
sessionJSON: `{"isLightMode":false}`,
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
wantResolvedWallpaper: "/tmp/blue.jpg",
wantDynamicOverrideUsed: false,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
homeDir := t.TempDir()
writeTestJSON(t, filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"), tt.settingsJSON)
writeTestJSON(t, filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"), tt.sessionJSON)
state, err := resolveGreeterThemeSyncState(homeDir)
if err != nil {
t.Fatalf("resolveGreeterThemeSyncState returned error: %v", err)
}
if got := state.effectiveColorsSource(homeDir); got != filepath.Join(homeDir, tt.wantSourcePath) {
t.Fatalf("effectiveColorsSource = %q, want %q", got, filepath.Join(homeDir, tt.wantSourcePath))
}
wantResolvedWallpaper := tt.wantResolvedWallpaper
if wantResolvedWallpaper != "" && !filepath.IsAbs(wantResolvedWallpaper) {
wantResolvedWallpaper = filepath.Join(homeDir, wantResolvedWallpaper)
}
if state.ResolvedGreeterWallpaperPath != wantResolvedWallpaper {
t.Fatalf("ResolvedGreeterWallpaperPath = %q, want %q", state.ResolvedGreeterWallpaperPath, wantResolvedWallpaper)
}
if state.UsesDynamicWallpaperOverride != tt.wantDynamicOverrideUsed {
t.Fatalf("UsesDynamicWallpaperOverride = %v, want %v", state.UsesDynamicWallpaperOverride, tt.wantDynamicOverrideUsed)
}
})
}
}
@@ -1,95 +0,0 @@
package providers
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
type MiracleProvider struct {
configPath string
}
func NewMiracleProvider(configPath string) *MiracleProvider {
if configPath == "" {
configDir, err := os.UserConfigDir()
if err == nil {
configPath = filepath.Join(configDir, "miracle-wm")
}
}
return &MiracleProvider{configPath: configPath}
}
func (m *MiracleProvider) Name() string {
return "miracle"
}
func (m *MiracleProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
config, err := ParseMiracleConfig(m.configPath)
if err != nil {
return nil, fmt.Errorf("failed to parse miracle-wm config: %w", err)
}
bindings := MiracleConfigToBindings(config)
categorizedBinds := make(map[string][]keybinds.Keybind)
for _, kb := range bindings {
category := m.categorizeAction(kb.Action)
bind := keybinds.Keybind{
Key: m.formatKey(kb),
Description: kb.Comment,
Action: kb.Action,
}
categorizedBinds[category] = append(categorizedBinds[category], bind)
}
return &keybinds.CheatSheet{
Title: "Miracle WM Keybinds",
Provider: m.Name(),
Binds: categorizedBinds,
}, nil
}
func (m *MiracleProvider) GetOverridePath() string {
expanded, err := utils.ExpandPath(m.configPath)
if err != nil {
return filepath.Join(m.configPath, "config.yaml")
}
return filepath.Join(expanded, "config.yaml")
}
func (m *MiracleProvider) formatKey(kb MiracleKeyBinding) string {
parts := make([]string, 0, len(kb.Mods)+1)
parts = append(parts, kb.Mods...)
parts = append(parts, kb.Key)
return strings.Join(parts, "+")
}
func (m *MiracleProvider) categorizeAction(action string) string {
switch {
case strings.HasPrefix(action, "select_workspace_") || strings.HasPrefix(action, "move_to_workspace_"):
return "Workspace"
case strings.Contains(action, "select_") || strings.Contains(action, "move_"):
return "Window"
case action == "toggle_resize" || strings.HasPrefix(action, "resize_"):
return "Window"
case action == "fullscreen" || action == "toggle_floating" || action == "quit_active_window" || action == "toggle_pinned_to_workspace":
return "Window"
case action == "toggle_tabbing" || action == "toggle_stacking" || action == "request_vertical" || action == "request_horizontal":
return "Layout"
case action == "quit_compositor":
return "System"
case action == "terminal":
return "Execute"
case strings.HasPrefix(action, "magnifier_"):
return "Accessibility"
case strings.HasPrefix(action, "dms ") || strings.Contains(action, "dms ipc"):
return "Execute"
default:
return "Execute"
}
}
@@ -1,320 +0,0 @@
package providers
import (
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"gopkg.in/yaml.v3"
)
type MiracleConfig struct {
Terminal string `yaml:"terminal"`
ActionKey string `yaml:"action_key"`
DefaultActionOverrides []MiracleActionOverride `yaml:"default_action_overrides"`
CustomActions []MiracleCustomAction `yaml:"custom_actions"`
}
type MiracleActionOverride struct {
Name string `yaml:"name"`
Action string `yaml:"action"`
Modifiers []string `yaml:"modifiers"`
Key string `yaml:"key"`
}
type MiracleCustomAction struct {
Command string `yaml:"command"`
Action string `yaml:"action"`
Modifiers []string `yaml:"modifiers"`
Key string `yaml:"key"`
}
type MiracleKeyBinding struct {
Mods []string
Key string
Action string
Comment string
}
var miracleDefaultBinds = []MiracleKeyBinding{
{Mods: []string{"Super"}, Key: "Return", Action: "terminal", Comment: "Open terminal"},
{Mods: []string{"Super"}, Key: "v", Action: "request_vertical", Comment: "Layout windows vertically"},
{Mods: []string{"Super"}, Key: "h", Action: "request_horizontal", Comment: "Layout windows horizontally"},
{Mods: []string{"Super"}, Key: "Up", Action: "select_up", Comment: "Select window above"},
{Mods: []string{"Super"}, Key: "Down", Action: "select_down", Comment: "Select window below"},
{Mods: []string{"Super"}, Key: "Left", Action: "select_left", Comment: "Select window left"},
{Mods: []string{"Super"}, Key: "Right", Action: "select_right", Comment: "Select window right"},
{Mods: []string{"Super", "Shift"}, Key: "Up", Action: "move_up", Comment: "Move window up"},
{Mods: []string{"Super", "Shift"}, Key: "Down", Action: "move_down", Comment: "Move window down"},
{Mods: []string{"Super", "Shift"}, Key: "Left", Action: "move_left", Comment: "Move window left"},
{Mods: []string{"Super", "Shift"}, Key: "Right", Action: "move_right", Comment: "Move window right"},
{Mods: []string{"Super"}, Key: "r", Action: "toggle_resize", Comment: "Toggle resize mode"},
{Mods: []string{"Super"}, Key: "f", Action: "fullscreen", Comment: "Toggle fullscreen"},
{Mods: []string{"Super", "Shift"}, Key: "q", Action: "quit_active_window", Comment: "Close window"},
{Mods: []string{"Super", "Shift"}, Key: "e", Action: "quit_compositor", Comment: "Exit compositor"},
{Mods: []string{"Super"}, Key: "Space", Action: "toggle_floating", Comment: "Toggle floating"},
{Mods: []string{"Super", "Shift"}, Key: "p", Action: "toggle_pinned_to_workspace", Comment: "Toggle pinned to workspace"},
{Mods: []string{"Super"}, Key: "w", Action: "toggle_tabbing", Comment: "Toggle tabbing layout"},
{Mods: []string{"Super"}, Key: "s", Action: "toggle_stacking", Comment: "Toggle stacking layout"},
{Mods: []string{"Super"}, Key: "1", Action: "select_workspace_0", Comment: "Workspace 1"},
{Mods: []string{"Super"}, Key: "2", Action: "select_workspace_1", Comment: "Workspace 2"},
{Mods: []string{"Super"}, Key: "3", Action: "select_workspace_2", Comment: "Workspace 3"},
{Mods: []string{"Super"}, Key: "4", Action: "select_workspace_3", Comment: "Workspace 4"},
{Mods: []string{"Super"}, Key: "5", Action: "select_workspace_4", Comment: "Workspace 5"},
{Mods: []string{"Super"}, Key: "6", Action: "select_workspace_5", Comment: "Workspace 6"},
{Mods: []string{"Super"}, Key: "7", Action: "select_workspace_6", Comment: "Workspace 7"},
{Mods: []string{"Super"}, Key: "8", Action: "select_workspace_7", Comment: "Workspace 8"},
{Mods: []string{"Super"}, Key: "9", Action: "select_workspace_8", Comment: "Workspace 9"},
{Mods: []string{"Super"}, Key: "0", Action: "select_workspace_9", Comment: "Workspace 10"},
{Mods: []string{"Super", "Shift"}, Key: "1", Action: "move_to_workspace_0", Comment: "Move to workspace 1"},
{Mods: []string{"Super", "Shift"}, Key: "2", Action: "move_to_workspace_1", Comment: "Move to workspace 2"},
{Mods: []string{"Super", "Shift"}, Key: "3", Action: "move_to_workspace_2", Comment: "Move to workspace 3"},
{Mods: []string{"Super", "Shift"}, Key: "4", Action: "move_to_workspace_3", Comment: "Move to workspace 4"},
{Mods: []string{"Super", "Shift"}, Key: "5", Action: "move_to_workspace_4", Comment: "Move to workspace 5"},
{Mods: []string{"Super", "Shift"}, Key: "6", Action: "move_to_workspace_5", Comment: "Move to workspace 6"},
{Mods: []string{"Super", "Shift"}, Key: "7", Action: "move_to_workspace_6", Comment: "Move to workspace 7"},
{Mods: []string{"Super", "Shift"}, Key: "8", Action: "move_to_workspace_7", Comment: "Move to workspace 8"},
{Mods: []string{"Super", "Shift"}, Key: "9", Action: "move_to_workspace_8", Comment: "Move to workspace 9"},
{Mods: []string{"Super", "Shift"}, Key: "0", Action: "move_to_workspace_9", Comment: "Move to workspace 10"},
}
func ParseMiracleConfig(configPath string) (*MiracleConfig, error) {
expanded, err := utils.ExpandPath(configPath)
if err != nil {
return nil, err
}
info, err := os.Stat(expanded)
if err != nil {
return nil, err
}
var configFile string
if info.IsDir() {
configFile = filepath.Join(expanded, "config.yaml")
} else {
configFile = expanded
}
data, err := os.ReadFile(configFile)
if err != nil {
return nil, err
}
var config MiracleConfig
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, err
}
if config.ActionKey == "" {
config.ActionKey = "meta"
}
return &config, nil
}
func resolveMiracleModifier(mod, actionKey string) string {
switch mod {
case "primary":
return resolveActionKey(actionKey)
case "alt", "alt_left", "alt_right":
return "Alt"
case "shift", "shift_left", "shift_right":
return "Shift"
case "ctrl", "ctrl_left", "ctrl_right":
return "Ctrl"
case "meta", "meta_left", "meta_right":
return "Super"
default:
return mod
}
}
func resolveActionKey(actionKey string) string {
switch actionKey {
case "meta":
return "Super"
case "alt":
return "Alt"
case "ctrl":
return "Ctrl"
default:
return "Super"
}
}
func miracleKeyCodeToName(keyCode string) string {
name := strings.TrimPrefix(keyCode, "KEY_")
name = strings.ToLower(name)
switch name {
case "enter":
return "Return"
case "space":
return "Space"
case "up":
return "Up"
case "down":
return "Down"
case "left":
return "Left"
case "right":
return "Right"
case "tab":
return "Tab"
case "escape", "esc":
return "Escape"
case "delete":
return "Delete"
case "backspace":
return "BackSpace"
case "home":
return "Home"
case "end":
return "End"
case "pageup":
return "Page_Up"
case "pagedown":
return "Page_Down"
case "print":
return "Print"
case "pause":
return "Pause"
case "volumeup":
return "XF86AudioRaiseVolume"
case "volumedown":
return "XF86AudioLowerVolume"
case "mute":
return "XF86AudioMute"
case "micmute":
return "XF86AudioMicMute"
case "brightnessup":
return "XF86MonBrightnessUp"
case "brightnessdown":
return "XF86MonBrightnessDown"
case "kbdillumup":
return "XF86KbdBrightnessUp"
case "kbdillumdown":
return "XF86KbdBrightnessDown"
case "comma":
return "comma"
case "minus":
return "minus"
case "equal":
return "equal"
}
if len(name) == 1 {
return name
}
return name
}
func MiracleConfigToBindings(config *MiracleConfig) []MiracleKeyBinding {
overridden := make(map[string]bool)
var bindings []MiracleKeyBinding
for _, override := range config.DefaultActionOverrides {
mods := make([]string, 0, len(override.Modifiers))
for _, mod := range override.Modifiers {
mods = append(mods, resolveMiracleModifier(mod, config.ActionKey))
}
bindings = append(bindings, MiracleKeyBinding{
Mods: mods,
Key: miracleKeyCodeToName(override.Key),
Action: override.Name,
Comment: miracleActionDescription(override.Name),
})
overridden[override.Name] = true
}
for _, def := range miracleDefaultBinds {
if overridden[def.Action] {
continue
}
bindings = append(bindings, def)
}
for _, custom := range config.CustomActions {
mods := make([]string, 0, len(custom.Modifiers))
for _, mod := range custom.Modifiers {
mods = append(mods, resolveMiracleModifier(mod, config.ActionKey))
}
bindings = append(bindings, MiracleKeyBinding{
Mods: mods,
Key: miracleKeyCodeToName(custom.Key),
Action: custom.Command,
Comment: custom.Command,
})
}
return bindings
}
func miracleActionDescription(action string) string {
switch action {
case "terminal":
return "Open terminal"
case "request_vertical":
return "Layout windows vertically"
case "request_horizontal":
return "Layout windows horizontally"
case "select_up":
return "Select window above"
case "select_down":
return "Select window below"
case "select_left":
return "Select window left"
case "select_right":
return "Select window right"
case "move_up":
return "Move window up"
case "move_down":
return "Move window down"
case "move_left":
return "Move window left"
case "move_right":
return "Move window right"
case "toggle_resize":
return "Toggle resize mode"
case "fullscreen":
return "Toggle fullscreen"
case "quit_active_window":
return "Close window"
case "quit_compositor":
return "Exit compositor"
case "toggle_floating":
return "Toggle floating"
case "toggle_pinned_to_workspace":
return "Toggle pinned to workspace"
case "toggle_tabbing":
return "Toggle tabbing layout"
case "toggle_stacking":
return "Toggle stacking layout"
case "magnifier_on":
return "Enable magnifier"
case "magnifier_off":
return "Disable magnifier"
case "magnifier_increase_size":
return "Increase magnifier area"
case "magnifier_decrease_size":
return "Decrease magnifier area"
case "magnifier_increase_scale":
return "Increase magnifier scale"
case "magnifier_decrease_scale":
return "Decrease magnifier scale"
}
if num, ok := strings.CutPrefix(action, "select_workspace_"); ok {
return "Workspace " + num
}
if num, ok := strings.CutPrefix(action, "move_to_workspace_"); ok {
return "Move to workspace " + num
}
return action
}
-2
View File
@@ -341,8 +341,6 @@ func (n *NiriProvider) buildActionFromNode(bindNode *document.Node) string {
val := arg.ValueString() val := arg.ValueString()
if val == "" { if val == "" {
parts = append(parts, `""`) parts = append(parts, `""`)
} else if strings.ContainsAny(val, " \t") {
parts = append(parts, `"`+strings.ReplaceAll(val, `"`, `\"`)+`"`)
} else { } else {
parts = append(parts, val) parts = append(parts, val)
} }
+11 -19
View File
@@ -3,7 +3,6 @@ package providers
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
@@ -19,21 +18,14 @@ func NewSwayProvider(configPath string) *SwayProvider {
_, scrollEnvSet := os.LookupEnv("SCROLLSOCK") _, scrollEnvSet := os.LookupEnv("SCROLLSOCK")
if configPath == "" { if configPath == "" {
configDir, err := os.UserConfigDir()
if err != nil {
configDir = ""
}
if scrollEnvSet { if scrollEnvSet {
if configDir != "" { configPath = "$HOME/.config/scroll"
configPath = filepath.Join(configDir, "scroll")
}
isScroll = true isScroll = true
} else { } else {
if configDir != "" { configPath = "$HOME/.config/sway"
configPath = filepath.Join(configDir, "sway")
}
} }
} else { } else {
// Determine isScroll based on the provided config path
isScroll = strings.Contains(configPath, "scroll") isScroll = strings.Contains(configPath, "scroll")
} }
@@ -44,16 +36,16 @@ func NewSwayProvider(configPath string) *SwayProvider {
} }
func (s *SwayProvider) Name() string { func (s *SwayProvider) Name() string {
if s == nil { if s != nil && s.isScroll {
if os.Getenv("SCROLLSOCK") != "" {
return "scroll"
}
return "sway"
}
if s.isScroll {
return "scroll" return "scroll"
} }
if s == nil {
_, ok := os.LookupEnv("SCROLLSOCK")
if ok {
return "scroll"
}
}
return "sway" return "sway"
} }
@@ -15,13 +15,8 @@ func TestSwayProviderName(t *testing.T) {
func TestSwayProviderDefaultPath(t *testing.T) { func TestSwayProviderDefaultPath(t *testing.T) {
provider := NewSwayProvider("") provider := NewSwayProvider("")
configDir, err := os.UserConfigDir() if provider.configPath != "$HOME/.config/sway" {
if err != nil { t.Errorf("configPath = %q, want %q", provider.configPath, "$HOME/.config/sway")
t.Skip("UserConfigDir not available")
}
expected := filepath.Join(configDir, "sway")
if provider.configPath != expected {
t.Errorf("configPath = %q, want %q", provider.configPath, expected)
} }
} }
+63 -182
View File
@@ -1,9 +1,7 @@
package matugen package matugen
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"math" "math"
"os" "os"
@@ -21,8 +19,6 @@ import (
"github.com/lucasb-eyer/go-colorful" "github.com/lucasb-eyer/go-colorful"
) )
var ErrNoChanges = errors.New("no color changes")
type ColorMode string type ColorMode string
const ( const (
@@ -37,7 +33,6 @@ const (
TemplateKindTerminal TemplateKindTerminal
TemplateKindGTK TemplateKindGTK
TemplateKindVSCode TemplateKindVSCode
TemplateKindEmacs
) )
type TemplateDef struct { type TemplateDef struct {
@@ -58,7 +53,7 @@ var templateRegistry = []TemplateDef{
{ID: "qt6ct", Commands: []string{"qt6ct"}, ConfigFile: "qt6ct.toml"}, {ID: "qt6ct", Commands: []string{"qt6ct"}, ConfigFile: "qt6ct.toml"},
{ID: "firefox", Commands: []string{"firefox"}, ConfigFile: "firefox.toml"}, {ID: "firefox", Commands: []string{"firefox"}, ConfigFile: "firefox.toml"},
{ID: "pywalfox", Commands: []string{"pywalfox"}, ConfigFile: "pywalfox.toml"}, {ID: "pywalfox", Commands: []string{"pywalfox"}, ConfigFile: "pywalfox.toml"},
{ID: "zenbrowser", Commands: []string{"zen", "zen-browser", "zen-beta", "zen-twilight"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"}, {ID: "zenbrowser", Commands: []string{"zen", "zen-browser"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"},
{ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigFile: "vesktop.toml"}, {ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigFile: "vesktop.toml"},
{ID: "equibop", Commands: []string{"equibop"}, ConfigFile: "equibop.toml"}, {ID: "equibop", Commands: []string{"equibop"}, ConfigFile: "equibop.toml"},
{ID: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal}, {ID: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal},
@@ -70,8 +65,7 @@ var templateRegistry = []TemplateDef{
{ID: "dgop", Commands: []string{"dgop"}, ConfigFile: "dgop.toml"}, {ID: "dgop", Commands: []string{"dgop"}, ConfigFile: "dgop.toml"},
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true}, {ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
{ID: "vscode", Kind: TemplateKindVSCode}, {ID: "vscode", Kind: TemplateKindVSCode},
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs}, {ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml"},
{ID: "zed", Commands: []string{"zed", "zeditor", "zedit"}, ConfigFile: "zed.toml"},
} }
func (c *ColorMode) GTKTheme() string { func (c *ColorMode) GTKTheme() string {
@@ -84,8 +78,7 @@ func (c *ColorMode) GTKTheme() string {
} }
var ( var (
matugenVersionMu sync.Mutex matugenVersionOnce sync.Once
matugenVersionOK bool
matugenSupportsCOE bool matugenSupportsCOE bool
matugenIsV4 bool matugenIsV4 bool
) )
@@ -100,7 +93,6 @@ type Options struct {
IconTheme string IconTheme string
MatugenType string MatugenType string
RunUserTemplates bool RunUserTemplates bool
ColorsOnly bool
StockColors string StockColors string
SyncModeWithPortal bool SyncModeWithPortal bool
TerminalsAlwaysDark bool TerminalsAlwaysDark bool
@@ -166,14 +158,8 @@ func Run(opts Options) error {
log.Infof("Building theme: %s %s (%s)", opts.Kind, opts.Value, opts.Mode) log.Infof("Building theme: %s %s (%s)", opts.Kind, opts.Value, opts.Mode)
changed, buildErr := buildOnce(&opts) if err := buildOnce(&opts); err != nil {
if buildErr != nil { return err
return buildErr
}
if !changed {
log.Info("No color changes detected, skipping refresh")
return ErrNoChanges
} }
if opts.SyncModeWithPortal { if opts.SyncModeWithPortal {
@@ -184,27 +170,25 @@ func Run(opts Options) error {
return nil return nil
} }
func buildOnce(opts *Options) (bool, error) { func buildOnce(opts *Options) error {
cfgFile, err := os.CreateTemp("", "matugen-config-*.toml") cfgFile, err := os.CreateTemp("", "matugen-config-*.toml")
if err != nil { if err != nil {
return false, fmt.Errorf("failed to create temp config: %w", err) return fmt.Errorf("failed to create temp config: %w", err)
} }
defer os.Remove(cfgFile.Name()) defer os.Remove(cfgFile.Name())
defer cfgFile.Close() defer cfgFile.Close()
tmpDir, err := os.MkdirTemp("", "matugen-templates-*") tmpDir, err := os.MkdirTemp("", "matugen-templates-*")
if err != nil { if err != nil {
return false, fmt.Errorf("failed to create temp dir: %w", err) return fmt.Errorf("failed to create temp dir: %w", err)
} }
defer os.RemoveAll(tmpDir) defer os.RemoveAll(tmpDir)
if err := buildMergedConfig(opts, cfgFile, tmpDir); err != nil { if err := buildMergedConfig(opts, cfgFile, tmpDir); err != nil {
return false, fmt.Errorf("failed to build config: %w", err) return fmt.Errorf("failed to build config: %w", err)
} }
cfgFile.Close() cfgFile.Close()
oldColors, _ := os.ReadFile(opts.ColorsOutput())
var primaryDark, primaryLight, surface string var primaryDark, primaryLight, surface string
var dank16JSON string var dank16JSON string
var importArgs []string var importArgs []string
@@ -216,7 +200,7 @@ func buildOnce(opts *Options) (bool, error) {
surface = extractNestedColor(opts.StockColors, "surface", "dark") surface = extractNestedColor(opts.StockColors, "surface", "dark")
if primaryDark == "" { if primaryDark == "" {
return false, fmt.Errorf("failed to extract primary dark from stock colors") return fmt.Errorf("failed to extract primary dark from stock colors")
} }
if primaryLight == "" { if primaryLight == "" {
primaryLight = primaryDark primaryLight = primaryDark
@@ -230,14 +214,14 @@ func buildOnce(opts *Options) (bool, error) {
args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()} args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()}
args = append(args, importArgs...) args = append(args, importArgs...)
if err := runMatugen(args); err != nil { if err := runMatugen(args); err != nil {
return false, err return err
} }
} else { } else {
log.Infof("Using dynamic theme from %s: %s", opts.Kind, opts.Value) log.Infof("Using dynamic theme from %s: %s", opts.Kind, opts.Value)
matJSON, err := runMatugenDryRun(opts) matJSON, err := runMatugenDryRun(opts)
if err != nil { if err != nil {
return false, fmt.Errorf("matugen dry-run failed: %w", err) return fmt.Errorf("matugen dry-run failed: %w", err)
} }
primaryDark = extractMatugenColor(matJSON, "primary", "dark") primaryDark = extractMatugenColor(matJSON, "primary", "dark")
@@ -245,7 +229,7 @@ func buildOnce(opts *Options) (bool, error) {
surface = extractMatugenColor(matJSON, "surface", "dark") surface = extractMatugenColor(matJSON, "surface", "dark")
if primaryDark == "" { if primaryDark == "" {
return false, fmt.Errorf("failed to extract primary color") return fmt.Errorf("failed to extract primary color")
} }
if primaryLight == "" { if primaryLight == "" {
primaryLight = primaryDark primaryLight = primaryDark
@@ -266,19 +250,10 @@ func buildOnce(opts *Options) (bool, error) {
args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()) args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name())
args = append(args, importArgs...) args = append(args, importArgs...)
if err := runMatugen(args); err != nil { if err := runMatugen(args); err != nil {
return false, err return err
} }
} }
newColors, _ := os.ReadFile(opts.ColorsOutput())
if bytes.Equal(oldColors, newColors) && len(oldColors) > 0 {
return false, nil
}
if opts.ColorsOnly {
return true, nil
}
if isDMSGTKActive(opts.ConfigDir) { if isDMSGTKActive(opts.ConfigDir) {
switch opts.Mode { switch opts.Mode {
case ColorModeLight: case ColorModeLight:
@@ -296,7 +271,7 @@ func buildOnce(opts *Options) (bool, error) {
signalTerminals(opts) signalTerminals(opts)
return true, nil return nil
} }
func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error { func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
@@ -336,10 +311,6 @@ output_path = '%s'
`, opts.ShellDir, opts.ColorsOutput()) `, opts.ShellDir, opts.ColorsOutput())
if opts.ColorsOnly {
return nil
}
homeDir, _ := os.UserHomeDir() homeDir, _ := os.UserHomeDir()
for _, tmpl := range templateRegistry { for _, tmpl := range templateRegistry {
if opts.ShouldSkipTemplate(tmpl.ID) { if opts.ShouldSkipTemplate(tmpl.ID) {
@@ -363,10 +334,6 @@ output_path = '%s'
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir) appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions"), opts.ShellDir) appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "vscode-insiders", filepath.Join(homeDir, ".vscode-insiders/extensions"), opts.ShellDir) appendVSCodeConfig(cfgFile, "vscode-insiders", filepath.Join(homeDir, ".vscode-insiders/extensions"), opts.ShellDir)
case TemplateKindEmacs:
if utils.EmacsConfigDir() != "" {
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
}
default: default:
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile) appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
} }
@@ -524,9 +491,6 @@ func substituteVars(content, shellDir string) string {
result = strings.ReplaceAll(result, "'CONFIG_DIR/", "'"+utils.XDGConfigHome()+"/") result = strings.ReplaceAll(result, "'CONFIG_DIR/", "'"+utils.XDGConfigHome()+"/")
result = strings.ReplaceAll(result, "'DATA_DIR/", "'"+utils.XDGDataHome()+"/") result = strings.ReplaceAll(result, "'DATA_DIR/", "'"+utils.XDGDataHome()+"/")
result = strings.ReplaceAll(result, "'CACHE_DIR/", "'"+utils.XDGCacheHome()+"/") result = strings.ReplaceAll(result, "'CACHE_DIR/", "'"+utils.XDGCacheHome()+"/")
if emacsDir := utils.EmacsConfigDir(); emacsDir != "" {
result = strings.ReplaceAll(result, "'EMACS_DIR/", "'"+emacsDir+"/")
}
return result return result
} }
@@ -547,159 +511,78 @@ func extractTOMLSection(content, startMarker, endMarker string) string {
return content[startIdx : startIdx+endIdx] return content[startIdx : startIdx+endIdx]
} }
type matugenFlags struct { func checkMatugenVersion() {
supportsCOE bool matugenVersionOnce.Do(func() {
isV4 bool 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)
}
})
} }
func detectMatugenVersion() (matugenFlags, error) { func runMatugen(args []string) error {
matugenVersionMu.Lock() checkMatugenVersion()
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 { if matugenSupportsCOE {
log.Debugf("Matugen %s detected: continue-on-error support enabled", versionStr) args = append([]string{"--continue-on-error"}, args...)
} }
if matugenIsV4 { if matugenIsV4 {
log.Debugf("Matugen %s detected: using v4 compatibility flags", versionStr)
}
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
}
func buildMatugenArgs(baseArgs []string, flags matugenFlags) []string {
args := make([]string, 0, len(baseArgs)+4)
if flags.supportsCOE {
args = append(args, "--continue-on-error")
}
args = append(args, baseArgs...)
if flags.isV4 {
args = append(args, "--source-color-index", "0") args = append(args, "--source-color-index", "0")
} }
return args
}
func runMatugen(baseArgs []string) error {
flags, err := detectMatugenVersion()
if err != nil {
return err
}
args := buildMatugenArgs(baseArgs, flags)
cmd := exec.Command("matugen", args...) cmd := exec.Command("matugen", args...)
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
runErr := cmd.Run() return cmd.Run()
if runErr == nil {
return nil
}
log.Warnf("Matugen failed (v4=%v): %v", flags.isV4, runErr)
newFlags, changed := redetectMatugenVersion(flags)
if !changed {
return runErr
}
log.Warnf("Matugen version changed (v4: %v -> %v), retrying", flags.isV4, newFlags.isV4)
args = buildMatugenArgs(baseArgs, newFlags)
retryCmd := exec.Command("matugen", args...)
retryCmd.Stdout = os.Stdout
retryCmd.Stderr = os.Stderr
return retryCmd.Run()
} }
func runMatugenDryRun(opts *Options) (string, error) { func runMatugenDryRun(opts *Options) (string, error) {
flags, err := detectMatugenVersion() checkMatugenVersion()
if err != nil {
return "", err
}
output, dryErr := execDryRun(opts, flags) var args []string
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 { switch opts.Kind {
case "hex": case "hex":
baseArgs = []string{"color", "hex", opts.Value} args = []string{"color", "hex", opts.Value}
default: default:
baseArgs = []string{opts.Kind, opts.Value} args = []string{opts.Kind, opts.Value}
} }
baseArgs = append(baseArgs, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run") args = append(args, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
if flags.isV4 { if matugenIsV4 {
baseArgs = append(baseArgs, "--source-color-index", "0", "--old-json-output") args = append(args, "--source-color-index", "0", "--old-json-output")
} }
cmd := exec.Command("matugen", baseArgs...) cmd := exec.Command("matugen", args...)
var stderr strings.Builder
cmd.Stderr = &stderr
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
if stderr.Len() > 0 { return "", err
return "", fmt.Errorf("matugen %v failed (v4=%v): %s", baseArgs, flags.isV4, strings.TrimSpace(stderr.String()))
}
return "", fmt.Errorf("matugen %v failed (v4=%v): %w", baseArgs, flags.isV4, err)
} }
return strings.ReplaceAll(string(output), "\n", ""), nil return strings.ReplaceAll(string(output), "\n", ""), nil
} }
@@ -936,8 +819,6 @@ func CheckTemplates(checker utils.AppChecker) []TemplateCheck {
detected = true detected = true
case tmpl.Kind == TemplateKindVSCode: case tmpl.Kind == TemplateKindVSCode:
detected = checkVSCodeExtension(homeDir) detected = checkVSCodeExtension(homeDir)
case tmpl.Kind == TemplateKindEmacs:
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks) && utils.EmacsConfigDir() != ""
default: default:
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks) detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks)
} }
-49
View File
@@ -3,7 +3,6 @@ package matugen
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils" mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils"
@@ -393,51 +392,3 @@ func TestSubstituteVars(t *testing.T) {
}) })
} }
} }
func TestBuildMergedConfigColorsOnly(t *testing.T) {
tempDir := t.TempDir()
shellDir := filepath.Join(tempDir, "shell")
configsDir := filepath.Join(shellDir, "matugen", "configs")
if err := os.MkdirAll(configsDir, 0o755); err != nil {
t.Fatalf("failed to create configs dir: %v", err)
}
baseConfig := "[config]\ncustom_keywords = []\n"
if err := os.WriteFile(filepath.Join(configsDir, "base.toml"), []byte(baseConfig), 0o644); err != nil {
t.Fatalf("failed to write base config: %v", err)
}
cfgFile, err := os.CreateTemp(tempDir, "merged-*.toml")
if err != nil {
t.Fatalf("failed to create temp config: %v", err)
}
defer os.Remove(cfgFile.Name())
defer cfgFile.Close()
opts := &Options{
ShellDir: shellDir,
ConfigDir: filepath.Join(tempDir, "config"),
StateDir: filepath.Join(tempDir, "state"),
ColorsOnly: true,
}
if err := buildMergedConfig(opts, cfgFile, filepath.Join(tempDir, "templates")); err != nil {
t.Fatalf("buildMergedConfig failed: %v", err)
}
if err := cfgFile.Close(); err != nil {
t.Fatalf("failed to close merged config: %v", err)
}
output, err := os.ReadFile(cfgFile.Name())
if err != nil {
t.Fatalf("failed to read merged config: %v", err)
}
content := string(output)
assert.Contains(t, content, "[templates.dank]")
assert.Contains(t, content, "output_path = '"+filepath.Join(opts.StateDir, "dms-colors.json")+"'")
assert.NotContains(t, content, "[templates.gtk]")
assert.False(t, strings.Contains(content, "output_path = 'CONFIG_DIR/"), "colors-only config should not emit app template outputs")
}
+3 -7
View File
@@ -2,7 +2,6 @@ package matugen
import ( import (
"context" "context"
"errors"
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -94,13 +93,10 @@ func (q *Queue) runWorker() {
err := Run(job.Options) err := Run(job.Options)
var result Result var result Result
switch { if err != nil {
case err == nil:
result = Result{Success: true}
case errors.Is(err, ErrNoChanges):
result = Result{Success: true}
default:
result = Result{Success: false, Error: err} result = Result{Success: false, Error: err}
} else {
result = Result{Success: true}
} }
q.finishJob(result) q.finishJob(result)
-10
View File
@@ -15,9 +15,6 @@ const (
notifyDest = "org.freedesktop.Notifications" notifyDest = "org.freedesktop.Notifications"
notifyPath = "/org/freedesktop/Notifications" notifyPath = "/org/freedesktop/Notifications"
notifyInterface = "org.freedesktop.Notifications" notifyInterface = "org.freedesktop.Notifications"
maxSummaryLen = 29
maxBodyLen = 80
) )
type Notification struct { type Notification struct {
@@ -42,13 +39,6 @@ func Send(n Notification) error {
n.Timeout = 5000 n.Timeout = 5000
} }
if len(n.Summary) > maxSummaryLen {
n.Summary = n.Summary[:maxSummaryLen-3] + "..."
}
if len(n.Body) > maxBodyLen {
n.Body = n.Body[:maxBodyLen-3] + "..."
}
var actions []string var actions []string
if n.FilePath != "" { if n.FilePath != "" {
actions = []string{ actions = []string{
@@ -258,7 +258,7 @@ func (i *ExtWorkspaceManagerV1) Dispatch(opcode uint32, fd int, data []byte) {
l := 0 l := 0
objectID := client.Uint32(data[l : l+4]) objectID := client.Uint32(data[l : l+4])
proxy := i.Context().GetProxy(objectID) proxy := i.Context().GetProxy(objectID)
if proxy != nil && !proxy.IsZombie() { if proxy != nil {
e.WorkspaceGroup = proxy.(*ExtWorkspaceGroupHandleV1) e.WorkspaceGroup = proxy.(*ExtWorkspaceGroupHandleV1)
} else { } else {
groupHandle := &ExtWorkspaceGroupHandleV1{} groupHandle := &ExtWorkspaceGroupHandleV1{}
@@ -278,7 +278,7 @@ func (i *ExtWorkspaceManagerV1) Dispatch(opcode uint32, fd int, data []byte) {
l := 0 l := 0
objectID := client.Uint32(data[l : l+4]) objectID := client.Uint32(data[l : l+4])
proxy := i.Context().GetProxy(objectID) proxy := i.Context().GetProxy(objectID)
if proxy != nil && !proxy.IsZombie() { if proxy != nil {
e.Workspace = proxy.(*ExtWorkspaceHandleV1) e.Workspace = proxy.(*ExtWorkspaceHandleV1)
} else { } else {
wsHandle := &ExtWorkspaceHandleV1{} wsHandle := &ExtWorkspaceHandleV1{}
+1 -28
View File
@@ -21,7 +21,6 @@ const (
CompositorNiri CompositorNiri
CompositorDWL CompositorDWL
CompositorScroll CompositorScroll
CompositorMiracle
) )
var detectedCompositor Compositor = -1 var detectedCompositor Compositor = -1
@@ -35,7 +34,6 @@ func DetectCompositor() Compositor {
niriSocket := os.Getenv("NIRI_SOCKET") niriSocket := os.Getenv("NIRI_SOCKET")
swaySocket := os.Getenv("SWAYSOCK") swaySocket := os.Getenv("SWAYSOCK")
scrollSocket := os.Getenv("SCROLLSOCK") scrollSocket := os.Getenv("SCROLLSOCK")
miracleSocket := os.Getenv("MIRACLESOCK")
switch { switch {
case niriSocket != "": case niriSocket != "":
@@ -48,11 +46,7 @@ func DetectCompositor() Compositor {
detectedCompositor = CompositorScroll detectedCompositor = CompositorScroll
return detectedCompositor return detectedCompositor
} }
case miracleSocket != "":
if _, err := os.Stat(miracleSocket); err == nil {
detectedCompositor = CompositorMiracle
return detectedCompositor
}
case swaySocket != "": case swaySocket != "":
if _, err := os.Stat(swaySocket); err == nil { if _, err := os.Stat(swaySocket); err == nil {
detectedCompositor = CompositorSway detectedCompositor = CompositorSway
@@ -266,25 +260,6 @@ func getScrollFocusedMonitor() string {
return "" return ""
} }
func getMiracleFocusedMonitor() string {
output, err := exec.Command("miraclemsg", "-t", "get_workspaces").Output()
if err != nil {
return ""
}
var workspaces []swayWorkspace
if err := json.Unmarshal(output, &workspaces); err != nil {
return ""
}
for _, ws := range workspaces {
if ws.Focused {
return ws.Output
}
}
return ""
}
type niriWorkspace struct { type niriWorkspace struct {
Output string `json:"output"` Output string `json:"output"`
IsFocused bool `json:"is_focused"` IsFocused bool `json:"is_focused"`
@@ -432,8 +407,6 @@ func GetFocusedMonitor() string {
return getSwayFocusedMonitor() return getSwayFocusedMonitor()
case CompositorScroll: case CompositorScroll:
return getScrollFocusedMonitor() return getScrollFocusedMonitor()
case CompositorMiracle:
return getMiracleFocusedMonitor()
case CompositorNiri: case CompositorNiri:
return getNiriFocusedMonitor() return getNiriFocusedMonitor()
case CompositorDWL: case CompositorDWL:
+1 -1
View File
@@ -108,7 +108,7 @@ func NewRegionSelector(s *Screenshoter) *RegionSelector {
screenshoter: s, screenshoter: s,
outputs: make(map[uint32]*WaylandOutput), outputs: make(map[uint32]*WaylandOutput),
preCapture: make(map[*WaylandOutput]*PreCapture), preCapture: make(map[*WaylandOutput]*PreCapture),
showCapturedCursor: s.config.Cursor == CursorOn, showCapturedCursor: true,
} }
} }
+8 -2
View File
@@ -453,7 +453,10 @@ func (s *Screenshoter) blitBuffer(dst, src *ShmBuffer, dstX, dstY int, yInverted
} }
func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult, error) { func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult, error) {
cursor := int32(s.config.Cursor) cursor := int32(0)
if s.config.IncludeCursor {
cursor = 1
}
frame, err := s.screencopy.CaptureOutput(cursor, output.wlOutput) frame, err := s.screencopy.CaptureOutput(cursor, output.wlOutput)
if err != nil { if err != nil {
@@ -621,7 +624,10 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
} }
} }
cursor := int32(s.config.Cursor) cursor := int32(0)
if s.config.IncludeCursor {
cursor = 1
}
frame, err := s.screencopy.CaptureOutputRegion(cursor, output.wlOutput, localX, localY, w, h) frame, err := s.screencopy.CaptureOutputRegion(cursor, output.wlOutput, localX, localY, w, h)
if err != nil { if err != nil {
+20 -27
View File
@@ -19,13 +19,6 @@ const (
FormatPPM FormatPPM
) )
type CursorMode int
const (
CursorOff CursorMode = iota
CursorOn
)
type Region struct { type Region struct {
X int32 `json:"x"` X int32 `json:"x"`
Y int32 `json:"y"` Y int32 `json:"y"`
@@ -49,29 +42,29 @@ type Output struct {
} }
type Config struct { type Config struct {
Mode Mode Mode Mode
OutputName string OutputName string
Cursor CursorMode IncludeCursor bool
Format Format Format Format
Quality int Quality int
OutputDir string OutputDir string
Filename string Filename string
Clipboard bool Clipboard bool
SaveFile bool SaveFile bool
Notify bool Notify bool
Stdout bool Stdout bool
} }
func DefaultConfig() Config { func DefaultConfig() Config {
return Config{ return Config{
Mode: ModeRegion, Mode: ModeRegion,
Cursor: CursorOff, IncludeCursor: false,
Format: FormatPNG, Format: FormatPNG,
Quality: 90, Quality: 90,
OutputDir: "", OutputDir: "",
Filename: "", Filename: "",
Clipboard: true, Clipboard: true,
SaveFile: true, SaveFile: true,
Notify: true, Notify: true,
} }
} }
-4
View File
@@ -311,10 +311,6 @@ func (m *Manager) handleDevicePropertiesChanged(path dbus.ObjectPath, changed ma
select { select {
case m.eventQueue <- func() { case m.eventQueue <- func() {
time.Sleep(300 * time.Millisecond) time.Sleep(300 * time.Millisecond)
log.Infof("[Bluetooth] Auto-trusting newly paired device: %s", devicePath)
if err := m.TrustDevice(devicePath, true); err != nil {
log.Warnf("[Bluetooth] Auto-trust failed: %v", err)
}
log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath) log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath)
if err := m.ConnectDevice(devicePath); err != nil { if err := m.ConnectDevice(devicePath); err != nil {
log.Warnf("[Bluetooth] Auto-connect failed: %v", err) log.Warnf("[Bluetooth] Auto-connect failed: %v", err)
+6 -24
View File
@@ -232,15 +232,8 @@ func (m *Manager) setupDataDeviceSync() {
return return
} }
prevOffer := m.currentOffer
m.currentOffer = offer m.currentOffer = offer
if prevOffer != nil && prevOffer != offer {
m.offerMutex.Lock()
delete(m.offerMimeTypes, prevOffer)
m.offerMutex.Unlock()
}
m.offerMutex.RLock() m.offerMutex.RLock()
mimes := m.offerMimeTypes[offer] mimes := m.offerMimeTypes[offer]
m.offerMutex.RUnlock() m.offerMutex.RUnlock()
@@ -594,26 +587,20 @@ func (m *Manager) uriListPreview(data []byte) (string, bool) {
uris = strings.Split(text, "\n") uris = strings.Split(text, "\n")
} }
if len(uris) > 1 {
return fmt.Sprintf("[[ %d files ]]", len(uris)), false
}
if len(uris) == 1 && strings.HasPrefix(uris[0], "file://") { if len(uris) == 1 && strings.HasPrefix(uris[0], "file://") {
filePath := strings.TrimPrefix(uris[0], "file://") filePath := strings.TrimPrefix(uris[0], "file://")
info, err := os.Stat(filePath) if info, err := os.Stat(filePath); err == nil && !info.IsDir() {
if err != nil || info.IsDir() {
return m.textPreview(data), false
}
cfg := m.getConfig()
if info.Size() <= cfg.MaxEntrySize {
if imgData, err := os.ReadFile(filePath); err == nil { if imgData, err := os.ReadFile(filePath); err == nil {
if config, imgFmt, err := image.DecodeConfig(bytes.NewReader(imgData)); err == nil { if config, imgFmt, err := image.DecodeConfig(bytes.NewReader(imgData)); err == nil {
return fmt.Sprintf("[[ file %s %s %dx%d ]]", filepath.Base(filePath), imgFmt, config.Width, config.Height), true return fmt.Sprintf("[[ file %s %s %dx%d ]]", filepath.Base(filePath), imgFmt, config.Width, config.Height), true
} }
} }
return fmt.Sprintf("[[ file %s ]]", filepath.Base(filePath)), false
} }
return fmt.Sprintf("[[ file %s ]]", filepath.Base(filePath)), false }
if len(uris) > 1 {
return fmt.Sprintf("[[ %d files ]]", len(uris)), false
} }
return m.textPreview(data), false return m.textPreview(data), false
@@ -636,11 +623,6 @@ func (m *Manager) tryReadImageFromURI(data []byte) ([]byte, string, bool) {
return nil, "", false return nil, "", false
} }
cfg := m.getConfig()
if info.Size() > cfg.MaxEntrySize {
return nil, "", false
}
imgData, err := os.ReadFile(filePath) imgData, err := os.ReadFile(filePath)
if err != nil { if err != nil {
return nil, "", false return nil, "", false
@@ -16,8 +16,4 @@ const (
dbusScreensaverPath = "/ScreenSaver" dbusScreensaverPath = "/ScreenSaver"
dbusScreensaverPath2 = "/org/freedesktop/ScreenSaver" dbusScreensaverPath2 = "/org/freedesktop/ScreenSaver"
dbusScreensaverInterface = "org.freedesktop.ScreenSaver" dbusScreensaverInterface = "org.freedesktop.ScreenSaver"
dbusGnomeScreensaverName = "org.gnome.ScreenSaver"
dbusGnomeScreensaverPath = "/org/gnome/ScreenSaver"
dbusGnomeScreensaverInterface = "org.gnome.ScreenSaver"
) )
@@ -191,12 +191,6 @@ func (m *Manager) Close() {
return true return true
}) })
m.screensaverSubscribers.Range(func(key string, ch chan ScreensaverState) bool {
close(ch)
m.screensaverSubscribers.Delete(key)
return true
})
if m.systemConn != nil { if m.systemConn != nil {
m.systemConn.Close() m.systemConn.Close()
} }
+46 -145
View File
@@ -1,7 +1,6 @@
package freedesktop package freedesktop
import ( import (
"fmt"
"path/filepath" "path/filepath"
"strings" "strings"
"sync/atomic" "sync/atomic"
@@ -16,51 +15,6 @@ type screensaverHandler struct {
manager *Manager manager *Manager
} }
func screensaverIntrospectIface(ifaceName string) introspect.Interface {
return introspect.Interface{
Name: ifaceName,
Methods: []introspect.Method{
{
Name: "Inhibit",
Args: []introspect.Arg{
{Name: "application_name", Type: "s", Direction: "in"},
{Name: "reason_for_inhibit", Type: "s", Direction: "in"},
{Name: "cookie", Type: "u", Direction: "out"},
},
},
{
Name: "UnInhibit",
Args: []introspect.Arg{
{Name: "cookie", Type: "u", Direction: "in"},
},
},
{
Name: "GetActive",
Args: []introspect.Arg{
{Name: "active", Type: "b", Direction: "out"},
},
},
{
Name: "SetActive",
Args: []introspect.Arg{
{Name: "active", Type: "b", Direction: "in"},
},
},
{
Name: "Lock",
},
},
Signals: []introspect.Signal{
{
Name: "ActiveChanged",
Args: []introspect.Arg{
{Name: "new_value", Type: "b"},
},
},
},
}
}
func (m *Manager) initializeScreensaver() error { func (m *Manager) initializeScreensaver() error {
if m.sessionConn == nil { if m.sessionConn == nil {
m.stateMutex.Lock() m.stateMutex.Lock()
@@ -69,71 +23,66 @@ func (m *Manager) initializeScreensaver() error {
return nil return nil
} }
handler := &screensaverHandler{manager: m} reply, err := m.sessionConn.RequestName(dbusScreensaverName, dbus.NameFlagDoNotQueue)
if err != nil {
m.screensaverFreedesktopClaimed = m.claimScreensaverName(handler, log.Warnf("Failed to request screensaver name: %v", err)
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.stateMutex.Lock()
m.state.Screensaver.Available = false m.state.Screensaver.Available = false
m.stateMutex.Unlock() m.stateMutex.Unlock()
return nil 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
}
introNode := &introspect.Node{
Name: dbusScreensaverPath,
Interfaces: []introspect.Interface{
introspect.IntrospectData,
{Name: dbusScreensaverInterface},
},
}
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode), dbusScreensaverPath, "org.freedesktop.DBus.Introspectable"); err != nil {
log.Warnf("Failed to export introspectable on %s: %v", dbusScreensaverPath, err)
}
introNode2 := &introspect.Node{
Name: dbusScreensaverPath2,
Interfaces: []introspect.Interface{
introspect.IntrospectData,
{Name: dbusScreensaverInterface},
},
}
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode2), dbusScreensaverPath2, "org.freedesktop.DBus.Introspectable"); err != nil {
log.Warnf("Failed to export introspectable on %s: %v", dbusScreensaverPath2, err)
}
go m.watchPeerDisconnects() go m.watchPeerDisconnects()
m.stateMutex.Lock() m.stateMutex.Lock()
m.state.Screensaver.Available = true m.state.Screensaver.Available = true
m.state.Screensaver.Active = false
m.state.Screensaver.Inhibited = false m.state.Screensaver.Inhibited = false
m.state.Screensaver.Inhibitors = []ScreensaverInhibitor{} m.state.Screensaver.Inhibitors = []ScreensaverInhibitor{}
m.stateMutex.Unlock() m.stateMutex.Unlock()
log.Info("Screensaver listener initialized") log.Info("Screensaver inhibit 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 return nil
} }
@@ -299,51 +248,3 @@ func (m *Manager) NotifyScreensaverSubscribers() {
return true return true
}) })
} }
func (h *screensaverHandler) GetActive() (bool, *dbus.Error) {
h.manager.stateMutex.RLock()
active := h.manager.state.Screensaver.Active
h.manager.stateMutex.RUnlock()
return active, nil
}
func (h *screensaverHandler) SetActive(active bool) *dbus.Error {
h.manager.SetScreenLockActive(active)
return nil
}
func (h *screensaverHandler) Lock() *dbus.Error {
h.manager.SetScreenLockActive(true)
return nil
}
func (m *Manager) SetScreenLockActive(active bool) {
m.stateMutex.Lock()
changed := m.state.Screensaver.Active != active
m.state.Screensaver.Active = active
m.stateMutex.Unlock()
if !changed {
return
}
log.Infof("Screen lock active changed: %v", active)
defer m.NotifyScreensaverSubscribers()
if m.sessionConn == nil {
return
}
if m.screensaverFreedesktopClaimed {
if err := m.sessionConn.Emit(dbusScreensaverPath, dbusScreensaverInterface+".ActiveChanged", active); err != nil {
log.Warnf("Failed to emit ActiveChanged on %s: %v", dbusScreensaverPath, err)
}
if err := m.sessionConn.Emit(dbusScreensaverPath2, dbusScreensaverInterface+".ActiveChanged", active); err != nil {
log.Warnf("Failed to emit ActiveChanged on %s: %v", dbusScreensaverPath2, err)
}
}
if m.screensaverGnomeClaimed {
if err := m.sessionConn.Emit(dbusGnomeScreensaverPath, dbusGnomeScreensaverInterface+".ActiveChanged", active); err != nil {
log.Warnf("Failed to emit ActiveChanged on %s: %v", dbusGnomeScreensaverPath, err)
}
}
}
@@ -1,102 +0,0 @@
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)
}
+10 -13
View File
@@ -39,7 +39,6 @@ type ScreensaverInhibitor struct {
type ScreensaverState struct { type ScreensaverState struct {
Available bool `json:"available"` Available bool `json:"available"`
Active bool `json:"active"`
Inhibited bool `json:"inhibited"` Inhibited bool `json:"inhibited"`
Inhibitors []ScreensaverInhibitor `json:"inhibitors"` Inhibitors []ScreensaverInhibitor `json:"inhibitors"`
} }
@@ -51,16 +50,14 @@ type FreedeskState struct {
} }
type Manager struct { type Manager struct {
state *FreedeskState state *FreedeskState
stateMutex sync.RWMutex stateMutex sync.RWMutex
systemConn *dbus.Conn systemConn *dbus.Conn
sessionConn *dbus.Conn sessionConn *dbus.Conn
accountsObj dbus.BusObject accountsObj dbus.BusObject
settingsObj dbus.BusObject settingsObj dbus.BusObject
currentUID uint64 currentUID uint64
subscribers syncmap.Map[string, chan FreedeskState] subscribers syncmap.Map[string, chan FreedeskState]
screensaverSubscribers syncmap.Map[string, chan ScreensaverState] screensaverSubscribers syncmap.Map[string, chan ScreensaverState]
screensaverCookieCounter uint32 screensaverCookieCounter uint32
screensaverFreedesktopClaimed bool
screensaverGnomeClaimed bool
} }
@@ -5,6 +5,5 @@ const (
dbusPath = "/org/freedesktop/login1" dbusPath = "/org/freedesktop/login1"
dbusManagerInterface = "org.freedesktop.login1.Manager" dbusManagerInterface = "org.freedesktop.login1.Manager"
dbusSessionInterface = "org.freedesktop.login1.Session" dbusSessionInterface = "org.freedesktop.login1.Session"
dbusUserInterface = "org.freedesktop.login1.User"
dbusPropsInterface = "org.freedesktop.DBus.Properties" dbusPropsInterface = "org.freedesktop.DBus.Properties"
) )
+9 -198
View File
@@ -17,8 +17,15 @@ func NewManager() (*Manager, error) {
return nil, fmt.Errorf("failed to connect to system bus: %w", err) return nil, fmt.Errorf("failed to connect to system bus: %w", err)
} }
sessionID := os.Getenv("XDG_SESSION_ID")
if sessionID == "" {
sessionID = "self"
}
m := &Manager{ m := &Manager{
state: &SessionState{}, state: &SessionState{
SessionID: sessionID,
},
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
@@ -53,13 +60,12 @@ func (m *Manager) initialize() error {
m.initializeFallbackDelay() m.initializeFallbackDelay()
sessionID, sessionPath, err := m.discoverSession() sessionPath, err := m.getSession(m.state.SessionID)
if err != nil { if err != nil {
return fmt.Errorf("failed to get session path: %w", err) return fmt.Errorf("failed to get session path: %w", err)
} }
m.stateMutex.Lock() m.stateMutex.Lock()
m.state.SessionID = sessionID
m.state.SessionPath = string(sessionPath) m.state.SessionPath = string(sessionPath)
m.sessionPath = sessionPath m.sessionPath = sessionPath
m.stateMutex.Unlock() m.stateMutex.Unlock()
@@ -73,41 +79,6 @@ func (m *Manager) initialize() error {
return nil return nil
} }
func (m *Manager) discoverSession() (string, dbus.ObjectPath, error) {
// 1. Explicit XDG_SESSION_ID
if id := os.Getenv("XDG_SESSION_ID"); id != "" {
if path, err := m.getSession(id); err == nil {
fmt.Fprintf(os.Stderr, "loginctl: using XDG_SESSION_ID=%s\n", id)
return id, path, nil
}
}
// 2. PID-based lookup (works when caller is inside a session cgroup)
if id, path, err := m.getSessionByPID(uint32(os.Getpid())); err == nil {
fmt.Fprintf(os.Stderr, "loginctl: found session %s via PID\n", id)
return id, path, nil
}
// 3. User's primary display session (handles UWSM and similar)
if id, path, err := m.getUserDisplaySession(); err == nil {
fmt.Fprintf(os.Stderr, "loginctl: found session %s via User.Display\n", id)
return id, path, nil
}
// 4. Score all sessions for current UID
if id, path, err := m.findBestSession(); err == nil {
fmt.Fprintf(os.Stderr, "loginctl: found session %s via ListSessions scoring\n", id)
return id, path, nil
}
// 5. Last resort: "self"
path, err := m.getSession("self")
if err != nil {
return "", "", fmt.Errorf("%w", err)
}
return "self", path, nil
}
func (m *Manager) getSession(id string) (dbus.ObjectPath, error) { func (m *Manager) getSession(id string) (dbus.ObjectPath, error) {
var out dbus.ObjectPath var out dbus.ObjectPath
err := m.managerObj.Call(dbusManagerInterface+".GetSession", 0, id).Store(&out) err := m.managerObj.Call(dbusManagerInterface+".GetSession", 0, id).Store(&out)
@@ -117,166 +88,6 @@ func (m *Manager) getSession(id string) (dbus.ObjectPath, error) {
return out, nil return out, nil
} }
func (m *Manager) getSessionByPID(pid uint32) (string, dbus.ObjectPath, error) {
var path dbus.ObjectPath
if err := m.managerObj.Call(dbusManagerInterface+".GetSessionByPID", 0, pid).Store(&path); err != nil {
return "", "", err
}
sessionObj := m.conn.Object(dbusDest, path)
var id dbus.Variant
if err := sessionObj.Call(dbusPropsInterface+".Get", 0, dbusSessionInterface, "Id").Store(&id); err != nil {
return "", "", err
}
return id.Value().(string), path, nil
}
func (m *Manager) getUserDisplaySession() (string, dbus.ObjectPath, error) {
uid := uint32(os.Getuid())
var userPath dbus.ObjectPath
if err := m.managerObj.Call(dbusManagerInterface+".GetUser", 0, uid).Store(&userPath); err != nil {
return "", "", err
}
userObj := m.conn.Object(dbusDest, userPath)
var display dbus.Variant
if err := userObj.Call(dbusPropsInterface+".Get", 0, dbusUserInterface, "Display").Store(&display); err != nil {
return "", "", err
}
pair, ok := display.Value().([]any)
if !ok || len(pair) < 2 {
return "", "", fmt.Errorf("unexpected Display format")
}
sessionID, _ := pair[0].(string)
sessionPath, _ := pair[1].(dbus.ObjectPath)
if sessionID == "" || sessionPath == "" {
return "", "", fmt.Errorf("empty Display session")
}
return sessionID, sessionPath, nil
}
type sessionCandidate struct {
id string
path dbus.ObjectPath
}
func (m *Manager) findBestSession() (string, dbus.ObjectPath, error) {
// ListSessions returns a(susso): [][]any where each entry is [id, uid, name, seat, path]
var raw [][]any
if err := m.managerObj.Call(dbusManagerInterface+".ListSessions", 0).Store(&raw); err != nil {
return "", "", err
}
uid := uint32(os.Getuid())
var candidates []sessionCandidate
for _, entry := range raw {
if len(entry) < 5 {
continue
}
entryUID, _ := entry[1].(uint32)
if entryUID != uid {
continue
}
id, _ := entry[0].(string)
path, _ := entry[4].(dbus.ObjectPath)
if id != "" && path != "" {
candidates = append(candidates, sessionCandidate{id: id, path: path})
}
}
if len(candidates) == 0 {
return "", "", fmt.Errorf("no sessions for uid %d", uid)
}
bestScore := -1
var best sessionCandidate
for _, c := range candidates {
score := m.scoreSession(c.path)
if score > bestScore {
bestScore = score
best = c
}
}
if bestScore < 0 {
return "", "", fmt.Errorf("no viable session found")
}
return best.id, best.path, nil
}
func (m *Manager) scoreSession(path dbus.ObjectPath) int {
obj := m.conn.Object(dbusDest, path)
var props map[string]dbus.Variant
if err := obj.Call(dbusPropsInterface+".GetAll", 0, dbusSessionInterface).Store(&props); err != nil {
return -1
}
getStr := func(key string) string {
if v, ok := props[key]; ok {
if s, ok := v.Value().(string); ok {
return s
}
}
return ""
}
getBool := func(key string) bool {
if v, ok := props[key]; ok {
if b, ok := v.Value().(bool); ok {
return b
}
}
return false
}
getUint32 := func(key string) uint32 {
if v, ok := props[key]; ok {
if u, ok := v.Value().(uint32); ok {
return u
}
}
return 0
}
class := getStr("Class")
if class != "user" {
return -1
}
if getBool("Remote") {
return -1
}
score := 0
if getBool("Active") {
score += 100
}
switch getStr("Type") {
case "wayland", "x11":
score += 80
case "tty":
score += 10
}
if v, ok := props["Seat"]; ok {
if seatArr, ok := v.Value().([]any); ok && len(seatArr) >= 1 {
if seat, ok := seatArr[0].(string); ok && seat != "" {
score += 40
if seat == "seat0" {
score += 10
}
}
}
}
if getUint32("VTNr") > 0 {
score += 20
}
return score
}
func (m *Manager) refreshSessionBinding() error { func (m *Manager) refreshSessionBinding() error {
if m.managerObj == nil || m.conn == nil { if m.managerObj == nil || m.conn == nil {
return fmt.Errorf("manager not fully initialized") return fmt.Errorf("manager not fully initialized")
@@ -32,10 +32,8 @@ type SecretAgent struct {
backend *NetworkManagerBackend backend *NetworkManagerBackend
} }
type ( type nmVariantMap map[string]dbus.Variant
nmVariantMap map[string]dbus.Variant type nmSettingMap map[string]nmVariantMap
nmSettingMap map[string]nmVariantMap
)
const introspectXML = ` const introspectXML = `
<node> <node>
@@ -310,63 +308,6 @@ func (a *SecretAgent) GetSecrets(
return out, nil return out, nil
} }
a.backend.cachedVPNCredsMu.Unlock() a.backend.cachedVPNCredsMu.Unlock()
a.backend.cachedGPSamlMu.Lock()
cachedGPSaml := a.backend.cachedGPSamlCookie
if cachedGPSaml != nil && cachedGPSaml.ConnectionUUID == connUuid {
a.backend.cachedGPSamlMu.Unlock()
log.Infof("[SecretAgent] Using cached GlobalProtect SAML cookie for %s", connUuid)
return buildGPSamlSecretsResponse(settingName, cachedGPSaml.Cookie, cachedGPSaml.Host, cachedGPSaml.Fingerprint), nil
}
a.backend.cachedGPSamlMu.Unlock()
if len(fields) == 1 && fields[0] == "gp-saml" {
gateway := ""
protocol := ""
if vpnSettings, ok := conn["vpn"]; ok {
if dataVariant, ok := vpnSettings["data"]; ok {
if dataMap, ok := dataVariant.Value().(map[string]string); ok {
if gw, ok := dataMap["gateway"]; ok {
gateway = gw
}
if proto, ok := dataMap["protocol"]; ok && proto != "" {
protocol = proto
}
}
}
}
if protocol != "gp" {
return nil, dbus.MakeFailedError(fmt.Errorf("gp-saml auth only supported for GlobalProtect (protocol=gp), got: %s", protocol))
}
log.Infof("[SecretAgent] Starting GlobalProtect SAML authentication for gateway=%s", gateway)
samlCtx, samlCancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer samlCancel()
authResult, err := a.backend.runGlobalProtectSAMLAuth(samlCtx, gateway, protocol)
if err != nil {
log.Warnf("[SecretAgent] GlobalProtect SAML authentication failed: %v", err)
return nil, dbus.MakeFailedError(fmt.Errorf("GlobalProtect SAML authentication failed: %w", err))
}
log.Infof("[SecretAgent] GlobalProtect SAML authentication successful, returning cookie to NetworkManager")
a.backend.cachedGPSamlMu.Lock()
a.backend.cachedGPSamlCookie = &cachedGPSamlCookie{
ConnectionUUID: connUuid,
Cookie: authResult.Cookie,
Host: authResult.Host,
User: authResult.User,
Fingerprint: authResult.Fingerprint,
}
a.backend.cachedGPSamlMu.Unlock()
return buildGPSamlSecretsResponse(settingName, authResult.Cookie, authResult.Host, authResult.Fingerprint), nil
}
} }
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
@@ -718,25 +659,12 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string {
switch { switch {
case strings.Contains(vpnService, "openconnect"): case strings.Contains(vpnService, "openconnect"):
protocol := dataMap["protocol"]
authType := dataMap["authtype"] authType := dataMap["authtype"]
username := dataMap["username"] userCert := dataMap["usercert"]
if authType == "cert" && strings.HasPrefix(userCert, "pkcs11:") {
if authType == "cert" && strings.HasPrefix(dataMap["usercert"], "pkcs11:") {
return []string{"key_pass"} return []string{"key_pass"}
} }
if dataMap["username"] == "" {
if needsExternalBrowserAuth(protocol, authType, username, dataMap) {
switch protocol {
case "gp":
log.Infof("[SecretAgent] GlobalProtect SAML auth detected")
return []string{"gp-saml"}
default:
log.Infof("[SecretAgent] External browser auth detected for protocol '%s' but only GlobalProtect (gp) SAML is currently supported, falling back to credentials", protocol)
}
}
if username == "" {
fields = []string{"username", "password"} fields = []string{"username", "password"}
} }
case strings.Contains(vpnService, "openvpn"): case strings.Contains(vpnService, "openvpn"):
@@ -755,31 +683,8 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string {
return fields return fields
} }
func needsExternalBrowserAuth(protocol, authType, username string, data map[string]string) bool {
if method, ok := data["saml-auth-method"]; ok {
if method == "REDIRECT" || method == "POST" {
return true
}
}
if authType != "" && authType != "password" && authType != "cert" {
return true
}
switch protocol {
case "gp":
if authType == "" && username == "" {
return true
}
}
return false
}
func vpnFieldMeta(field, vpnService string) (label string, isSecret bool) { func vpnFieldMeta(field, vpnService string) (label string, isSecret bool) {
switch field { switch field {
case "gp-saml":
return "GlobalProtect SAML/SSO", false
case "key_pass": case "key_pass":
return "PIN", true return "PIN", true
case "password": case "password":
@@ -880,18 +785,3 @@ func reasonFromFlags(flags uint32) string {
} }
return "required" return "required"
} }
func buildGPSamlSecretsResponse(settingName, cookie, host, fingerprint string) nmSettingMap {
out := nmSettingMap{}
vpnSec := nmVariantMap{}
secrets := map[string]string{
"cookie": cookie,
"gateway": host,
"gwcert": fingerprint,
}
vpnSec["secrets"] = dbus.MakeVariant(secrets)
out[settingName] = vpnSec
return out
}
@@ -1,355 +0,0 @@
package network
import (
"testing"
"github.com/godbus/dbus/v5"
"github.com/stretchr/testify/assert"
)
func TestNeedsExternalBrowserAuth(t *testing.T) {
tests := []struct {
name string
protocol string
authType string
username string
data map[string]string
expected bool
}{
{
name: "GP with saml-auth-method REDIRECT",
protocol: "gp",
authType: "password",
username: "user",
data: map[string]string{"saml-auth-method": "REDIRECT"},
expected: true,
},
{
name: "GP with saml-auth-method POST",
protocol: "gp",
authType: "password",
username: "user",
data: map[string]string{"saml-auth-method": "POST"},
expected: true,
},
{
name: "GP with no authtype and no username",
protocol: "gp",
authType: "",
username: "",
data: map[string]string{},
expected: true,
},
{
name: "GP with username and password authtype",
protocol: "gp",
authType: "password",
username: "john",
data: map[string]string{},
expected: false,
},
{
name: "GP with username but no authtype",
protocol: "gp",
authType: "",
username: "john",
data: map[string]string{},
expected: false,
},
{
name: "GP with authtype but no username - should detect SAML",
protocol: "gp",
authType: "",
username: "",
data: map[string]string{},
expected: true,
},
{
name: "pulse with SAML",
protocol: "pulse",
authType: "",
username: "",
data: map[string]string{"saml-auth-method": "REDIRECT"},
expected: true,
},
{
name: "fortinet with non-password authtype",
protocol: "fortinet",
authType: "saml",
username: "",
data: map[string]string{},
expected: true,
},
{
name: "anyconnect with cert",
protocol: "anyconnect",
authType: "cert",
username: "",
data: map[string]string{},
expected: false,
},
{
name: "anyconnect with password",
protocol: "anyconnect",
authType: "password",
username: "user",
data: map[string]string{},
expected: false,
},
{
name: "empty protocol",
protocol: "",
authType: "",
username: "",
data: map[string]string{},
expected: false,
},
{
name: "GP with cert authtype",
protocol: "gp",
authType: "cert",
username: "",
data: map[string]string{},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := needsExternalBrowserAuth(tt.protocol, tt.authType, tt.username, tt.data)
assert.Equal(t, tt.expected, result)
})
}
}
func TestBuildGPSamlSecretsResponse(t *testing.T) {
tests := []struct {
name string
settingName string
cookie string
host string
fingerprint string
}{
{
name: "all fields populated",
settingName: "vpn",
cookie: "authcookie=abc123&portal=GATE",
host: "vpn.example.com",
fingerprint: "pin-sha256:ABCD1234",
},
{
name: "empty fingerprint",
settingName: "vpn",
cookie: "authcookie=xyz",
host: "10.0.0.1",
fingerprint: "",
},
{
name: "complex cookie with special chars",
settingName: "vpn",
cookie: "authcookie=077058d3bc81&portal=PANGP_GW_01-N&user=john.doe@example.com&domain=Default&preferred-ip=192.168.1.100",
host: "connect.seclore.com",
fingerprint: "pin-sha256:xp3scfzy3rOgQEXnfPiYKrUk7D66a8b8O+gEXaMPleE=",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := buildGPSamlSecretsResponse(tt.settingName, tt.cookie, tt.host, tt.fingerprint)
assert.NotNil(t, result)
assert.Contains(t, result, tt.settingName)
vpnSec := result[tt.settingName]
assert.NotNil(t, vpnSec)
secretsVariant, ok := vpnSec["secrets"]
assert.True(t, ok, "secrets key should exist")
secrets, ok := secretsVariant.Value().(map[string]string)
assert.True(t, ok, "secrets should be map[string]string")
assert.Equal(t, tt.cookie, secrets["cookie"])
assert.Equal(t, tt.host, secrets["gateway"])
assert.Equal(t, tt.fingerprint, secrets["gwcert"])
})
}
}
func TestVpnFieldMeta_GPSaml(t *testing.T) {
label, isSecret := vpnFieldMeta("gp-saml", "org.freedesktop.NetworkManager.openconnect")
assert.Equal(t, "GlobalProtect SAML/SSO", label)
assert.False(t, isSecret, "gp-saml should not be marked as secret")
}
func TestVpnFieldMeta_StandardFields(t *testing.T) {
tests := []struct {
field string
vpnService string
expectedLabel string
expectedSecret bool
}{
{
field: "username",
vpnService: "org.freedesktop.NetworkManager.openconnect",
expectedLabel: "Username",
expectedSecret: false,
},
{
field: "password",
vpnService: "org.freedesktop.NetworkManager.openconnect",
expectedLabel: "Password",
expectedSecret: true,
},
{
field: "key_pass",
vpnService: "org.freedesktop.NetworkManager.openconnect",
expectedLabel: "PIN",
expectedSecret: true,
},
}
for _, tt := range tests {
t.Run(tt.field, func(t *testing.T) {
label, isSecret := vpnFieldMeta(tt.field, tt.vpnService)
assert.Equal(t, tt.expectedLabel, label)
assert.Equal(t, tt.expectedSecret, isSecret)
})
}
}
func TestInferVPNFields_GPSaml(t *testing.T) {
tests := []struct {
name string
vpnService string
dataMap map[string]string
expectedLen int
shouldHave []string
}{
{
name: "GP with no authtype and no username - should require SAML",
vpnService: "org.freedesktop.NetworkManager.openconnect",
dataMap: map[string]string{
"protocol": "gp",
"gateway": "vpn.example.com",
},
expectedLen: 1,
shouldHave: []string{"gp-saml"},
},
{
name: "GP with saml-auth-method REDIRECT",
vpnService: "org.freedesktop.NetworkManager.openconnect",
dataMap: map[string]string{
"protocol": "gp",
"gateway": "vpn.example.com",
"saml-auth-method": "REDIRECT",
"username": "john",
},
expectedLen: 1,
shouldHave: []string{"gp-saml"},
},
{
name: "GP with saml-auth-method POST",
vpnService: "org.freedesktop.NetworkManager.openconnect",
dataMap: map[string]string{
"protocol": "gp",
"gateway": "vpn.example.com",
"saml-auth-method": "POST",
},
expectedLen: 1,
shouldHave: []string{"gp-saml"},
},
{
name: "GP with username and password authtype - should use credentials",
vpnService: "org.freedesktop.NetworkManager.openconnect",
dataMap: map[string]string{
"protocol": "gp",
"gateway": "vpn.example.com",
"authtype": "password",
"username": "john",
},
expectedLen: 1,
shouldHave: []string{"password"},
},
{
name: "GP with username but no authtype - password only",
vpnService: "org.freedesktop.NetworkManager.openconnect",
dataMap: map[string]string{
"protocol": "gp",
"gateway": "vpn.example.com",
"username": "john",
},
expectedLen: 1,
shouldHave: []string{"password"},
},
{
name: "GP with PKCS11 cert",
vpnService: "org.freedesktop.NetworkManager.openconnect",
dataMap: map[string]string{
"protocol": "gp",
"gateway": "vpn.example.com",
"authtype": "cert",
"usercert": "pkcs11:model=PKCS%2315%20emulated;manufacturer=piv_II",
},
expectedLen: 1,
shouldHave: []string{"key_pass"},
},
{
name: "non-GP protocol (anyconnect)",
vpnService: "org.freedesktop.NetworkManager.openconnect",
dataMap: map[string]string{
"protocol": "anyconnect",
"gateway": "vpn.example.com",
},
expectedLen: 2,
shouldHave: []string{"username", "password"},
},
{
name: "OpenVPN with username",
vpnService: "org.freedesktop.NetworkManager.openvpn",
dataMap: map[string]string{
"connection-type": "password",
"username": "john",
},
expectedLen: 1,
shouldHave: []string{"password"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Convert dataMap to nmVariantMap
vpnSettings := make(nmVariantMap)
vpnSettings["data"] = dbus.MakeVariant(tt.dataMap)
vpnSettings["service-type"] = dbus.MakeVariant(tt.vpnService)
conn := make(map[string]nmVariantMap)
conn["vpn"] = vpnSettings
fields := inferVPNFields(conn, tt.vpnService)
assert.Len(t, fields, tt.expectedLen, "unexpected number of fields")
if len(tt.shouldHave) > 0 {
for _, expected := range tt.shouldHave {
assert.Contains(t, fields, expected, "should contain field: %s", expected)
}
}
})
}
}
func TestNmVariantMap(t *testing.T) {
// Test that nmVariantMap and nmSettingMap work correctly
settingMap := make(nmSettingMap)
variantMap := make(nmVariantMap)
variantMap["test-key"] = dbus.MakeVariant("test-value")
settingMap["test-setting"] = variantMap
assert.Contains(t, settingMap, "test-setting")
assert.Contains(t, settingMap["test-setting"], "test-key")
value := settingMap["test-setting"]["test-key"].Value()
assert.Equal(t, "test-value", value)
}
@@ -69,14 +69,12 @@ type NetworkManagerBackend struct {
lastFailedTime int64 lastFailedTime int64
failedMutex sync.RWMutex failedMutex sync.RWMutex
pendingVPNSave *pendingVPNCredentials pendingVPNSave *pendingVPNCredentials
pendingVPNSaveMu sync.Mutex pendingVPNSaveMu sync.Mutex
cachedVPNCreds *cachedVPNCredentials cachedVPNCreds *cachedVPNCredentials
cachedVPNCredsMu sync.Mutex cachedVPNCredsMu sync.Mutex
cachedPKCS11PIN *cachedPKCS11PIN cachedPKCS11PIN *cachedPKCS11PIN
cachedPKCS11Mu sync.Mutex cachedPKCS11Mu sync.Mutex
cachedGPSamlCookie *cachedGPSamlCookie
cachedGPSamlMu sync.Mutex
onStateChange func() onStateChange func()
} }
@@ -99,14 +97,6 @@ type cachedPKCS11PIN struct {
PIN string PIN string
} }
type cachedGPSamlCookie struct {
ConnectionUUID string
Cookie string
Host string
User string
Fingerprint string
}
func NewNetworkManagerBackend(nmConn ...gonetworkmanager.NetworkManager) (*NetworkManagerBackend, error) { func NewNetworkManagerBackend(nmConn ...gonetworkmanager.NetworkManager) (*NetworkManagerBackend, error) {
var nm gonetworkmanager.NetworkManager var nm gonetworkmanager.NetworkManager
var err error var err error
@@ -1,203 +0,0 @@
package network
import (
"bufio"
"context"
"fmt"
"os/exec"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
type gpSamlAuthResult struct {
Cookie string
Host string
User string
Fingerprint string
}
// runGlobalProtectSAMLAuth handles GlobalProtect SAML/SSO authentication using gp-saml-gui.
// Only supports protocol=gp. Other protocols need their own implementations.
func (b *NetworkManagerBackend) runGlobalProtectSAMLAuth(ctx context.Context, gateway, protocol string) (*gpSamlAuthResult, error) {
if gateway == "" {
return nil, fmt.Errorf("GP SAML auth: gateway is empty")
}
if protocol != "gp" {
return nil, fmt.Errorf("only GlobalProtect (protocol=gp) SAML is supported, got: %s", protocol)
}
log.Infof("[GP-SAML] Starting GlobalProtect SAML authentication with gp-saml-gui for gateway=%s", gateway)
gpSamlPath, err := exec.LookPath("gp-saml-gui")
if err != nil {
return nil, fmt.Errorf("GlobalProtect SAML requires gp-saml-gui (install: pip install gp-saml-gui): %w", err)
}
args := []string{
"--gateway",
"--allow-insecure-crypto",
gateway,
}
cmd := exec.CommandContext(ctx, gpSamlPath, args...)
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, fmt.Errorf("GP SAML auth: failed to create stdout pipe: %w", err)
}
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, fmt.Errorf("GP SAML auth: failed to create stderr pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("GP SAML auth: failed to start gp-saml-gui: %w", err)
}
go func() {
scanner := bufio.NewScanner(stderr)
for scanner.Scan() {
log.Debugf("[GP-SAML] gp-saml-gui: %s", scanner.Text())
}
}()
result := &gpSamlAuthResult{Host: gateway}
var allOutput []string
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
line := scanner.Text()
allOutput = append(allOutput, line)
log.Infof("[GP-SAML] stdout: %s", line)
switch {
case strings.HasPrefix(line, "COOKIE="):
result.Cookie = unshellQuote(strings.TrimPrefix(line, "COOKIE="))
case strings.HasPrefix(line, "HOST="):
result.Host = unshellQuote(strings.TrimPrefix(line, "HOST="))
case strings.HasPrefix(line, "USER="):
result.User = unshellQuote(strings.TrimPrefix(line, "USER="))
case strings.HasPrefix(line, "FINGERPRINT="):
result.Fingerprint = unshellQuote(strings.TrimPrefix(line, "FINGERPRINT="))
default:
parseGPSamlFromCommandLine(line, result)
}
}
if err := cmd.Wait(); err != nil {
if ctx.Err() != nil {
return nil, fmt.Errorf("GP SAML auth timed out or was cancelled: %w", ctx.Err())
}
if result.Cookie == "" {
return nil, fmt.Errorf("GP SAML auth failed: %w (output: %s)", err, strings.Join(allOutput, "\n"))
}
log.Warnf("[GP-SAML] gp-saml-gui exited with error but cookie was captured: %v", err)
}
if result.Cookie == "" {
return nil, fmt.Errorf("GP SAML auth: no cookie in gp-saml-gui output")
}
log.Infof("[GP-SAML] Got prelogin-cookie from gp-saml-gui, converting to openconnect cookie via --authenticate")
// Convert prelogin-cookie to full openconnect cookie format
ocResult, err := convertGPPreloginCookie(ctx, gateway, result.Cookie, result.User)
if err != nil {
return nil, fmt.Errorf("GP SAML auth: failed to convert prelogin-cookie: %w", err)
}
result.Cookie = ocResult.Cookie
result.Host = ocResult.Host
result.Fingerprint = ocResult.Fingerprint
log.Infof("[GP-SAML] Authentication successful: user=%s, host=%s, cookie_len=%d, has_fingerprint=%v",
result.User, result.Host, len(result.Cookie), result.Fingerprint != "")
return result, nil
}
func convertGPPreloginCookie(ctx context.Context, gateway, preloginCookie, user string) (*gpSamlAuthResult, error) {
ocPath, err := exec.LookPath("openconnect")
if err != nil {
return nil, fmt.Errorf("openconnect not found: %w", err)
}
args := []string{
"--protocol=gp",
"--usergroup=gateway:prelogin-cookie",
"--user=" + user,
"--passwd-on-stdin",
"--allow-insecure-crypto",
"--authenticate",
gateway,
}
cmd := exec.CommandContext(ctx, ocPath, args...)
cmd.Stdin = strings.NewReader(preloginCookie)
output, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("openconnect --authenticate failed: %w\noutput: %s", err, string(output))
}
result := &gpSamlAuthResult{}
for _, line := range strings.Split(string(output), "\n") {
line = strings.TrimSpace(line)
switch {
case strings.HasPrefix(line, "COOKIE="):
result.Cookie = unshellQuote(strings.TrimPrefix(line, "COOKIE="))
case strings.HasPrefix(line, "HOST="):
result.Host = unshellQuote(strings.TrimPrefix(line, "HOST="))
case strings.HasPrefix(line, "FINGERPRINT="):
result.Fingerprint = unshellQuote(strings.TrimPrefix(line, "FINGERPRINT="))
case strings.HasPrefix(line, "CONNECT_URL="):
connectURL := unshellQuote(strings.TrimPrefix(line, "CONNECT_URL="))
if connectURL != "" && result.Host == "" {
result.Host = connectURL
}
}
}
if result.Cookie == "" {
return nil, fmt.Errorf("no COOKIE in openconnect --authenticate output: %s", string(output))
}
log.Infof("[GP-SAML] openconnect --authenticate: cookie_len=%d, host=%s, fingerprint=%s",
len(result.Cookie), result.Host, result.Fingerprint)
return result, nil
}
func unshellQuote(s string) string {
if len(s) >= 2 {
if (s[0] == '\'' && s[len(s)-1] == '\'') ||
(s[0] == '"' && s[len(s)-1] == '"') {
return s[1 : len(s)-1]
}
}
return s
}
func parseGPSamlFromCommandLine(line string, result *gpSamlAuthResult) {
if !strings.Contains(line, "openconnect") {
return
}
for _, part := range strings.Fields(line) {
switch {
case strings.HasPrefix(part, "--cookie="):
if result.Cookie == "" {
result.Cookie = strings.TrimPrefix(part, "--cookie=")
}
case strings.HasPrefix(part, "--servercert="):
if result.Fingerprint == "" {
result.Fingerprint = strings.TrimPrefix(part, "--servercert=")
}
case strings.HasPrefix(part, "--user="):
if result.User == "" {
result.User = strings.TrimPrefix(part, "--user=")
}
}
}
}
@@ -1,169 +0,0 @@
package network
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestUnshellQuote(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "single quoted",
input: "'hello world'",
expected: "hello world",
},
{
name: "double quoted",
input: `"hello world"`,
expected: "hello world",
},
{
name: "unquoted",
input: "hello",
expected: "hello",
},
{
name: "empty single quotes",
input: "''",
expected: "",
},
{
name: "empty double quotes",
input: `""`,
expected: "",
},
{
name: "single quote only",
input: "'",
expected: "'",
},
{
name: "mismatched quotes",
input: "'hello\"",
expected: "'hello\"",
},
{
name: "with special chars",
input: "'cookie=abc123&user=john'",
expected: "cookie=abc123&user=john",
},
{
name: "complex cookie",
input: `'authcookie=077058d3bc81&portal=PANGP_GW_01-N&user=john.doe@example.com&domain=Default&preferred-ip=192.168.1.100'`,
expected: "authcookie=077058d3bc81&portal=PANGP_GW_01-N&user=john.doe@example.com&domain=Default&preferred-ip=192.168.1.100",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := unshellQuote(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestParseGPSamlFromCommandLine(t *testing.T) {
tests := []struct {
name string
line string
initialResult *gpSamlAuthResult
expectedCookie string
expectedUser string
expectedFP string
}{
{
name: "full openconnect command",
line: "openconnect --protocol=gp --cookie=AUTH123 --servercert=pin-sha256:ABC --user=john",
initialResult: &gpSamlAuthResult{},
expectedCookie: "AUTH123",
expectedUser: "john",
expectedFP: "pin-sha256:ABC",
},
{
name: "with equals signs in cookie",
line: "openconnect --cookie=authcookie=xyz123&portal=GATE --user=jane",
initialResult: &gpSamlAuthResult{},
expectedCookie: "authcookie=xyz123&portal=GATE",
expectedUser: "jane",
expectedFP: "",
},
{
name: "non-openconnect line",
line: "some other output",
initialResult: &gpSamlAuthResult{},
expectedCookie: "",
expectedUser: "",
expectedFP: "",
},
{
name: "preserves existing values",
line: "openconnect --user=newuser",
initialResult: &gpSamlAuthResult{Cookie: "existing", Fingerprint: "existing-fp"},
expectedCookie: "existing",
expectedUser: "newuser",
expectedFP: "existing-fp",
},
{
name: "only updates empty fields",
line: "openconnect --cookie=NEW --user=NEW",
initialResult: &gpSamlAuthResult{Cookie: "OLD"},
expectedCookie: "OLD",
expectedUser: "NEW",
expectedFP: "",
},
{
name: "real gp-saml-gui output",
line: "openconnect --protocol=gp --user=john.doe@example.com --os=linux-64 --usergroup=gateway:prelogin-cookie --passwd-on-stdin",
initialResult: &gpSamlAuthResult{},
expectedCookie: "",
expectedUser: "john.doe@example.com",
expectedFP: "",
},
{
name: "with server cert flag",
line: "openconnect --servercert=pin-sha256:xp3scfzy3rOgQEXnfPiYKrUk7D66a8b8O+gEXaMPleE= vpn.example.com",
initialResult: &gpSamlAuthResult{},
expectedCookie: "",
expectedUser: "",
expectedFP: "pin-sha256:xp3scfzy3rOgQEXnfPiYKrUk7D66a8b8O+gEXaMPleE=",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.initialResult
parseGPSamlFromCommandLine(tt.line, result)
assert.Equal(t, tt.expectedCookie, result.Cookie, "cookie mismatch")
assert.Equal(t, tt.expectedUser, result.User, "user mismatch")
assert.Equal(t, tt.expectedFP, result.Fingerprint, "fingerprint mismatch")
})
}
}
func TestParseGPSamlFromCommandLine_MultipleLines(t *testing.T) {
// Simulate gp-saml-gui output with command line suggestion
lines := []string{
"",
"SAML REDIRECT",
"Got SAML Login URL",
"POST to ACS endpoint...",
"Got 'prelogin-cookie': 'FAKE_cookie_12345'",
"openconnect --protocol=gp --user=john.doe@example.com --usergroup=gateway:prelogin-cookie --passwd-on-stdin vpn.example.com",
"",
}
result := &gpSamlAuthResult{}
for _, line := range lines {
parseGPSamlFromCommandLine(line, result)
}
assert.Equal(t, "john.doe@example.com", result.User)
assert.Empty(t, result.Cookie, "cookie should not be parsed from command line")
assert.Empty(t, result.Fingerprint)
}
@@ -304,51 +304,6 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
if err := b.handleOpenVPNUsernameAuth(targetConn, connName, targetUUID, vpnServiceType); err != nil { if err := b.handleOpenVPNUsernameAuth(targetConn, connName, targetUUID, vpnServiceType); err != nil {
return err return err
} }
case "gp_saml":
gateway := vpnData["gateway"]
protocol := vpnData["protocol"]
if protocol != "gp" {
return fmt.Errorf("GlobalProtect SAML authentication only supported for protocol=gp, got: %s", protocol)
}
log.Infof("[ConnectVPN] GlobalProtect SAML/SSO authentication required for %s (gateway=%s)", connName, gateway)
samlCtx, samlCancel := context.WithTimeout(context.Background(), 5*time.Minute)
authResult, err := b.runGlobalProtectSAMLAuth(samlCtx, gateway, protocol)
samlCancel()
if err != nil {
errMsg := err.Error()
switch {
case strings.Contains(errMsg, "not installed"):
return fmt.Errorf("gp-saml-gui is not installed (required for GlobalProtect SAML/SSO VPN)")
case strings.Contains(errMsg, "timed out") || strings.Contains(errMsg, "cancelled"):
return fmt.Errorf("GlobalProtect SAML authentication timed out — please try again")
case strings.Contains(errMsg, "no cookie"):
return fmt.Errorf("GlobalProtect SAML login did not complete — browser was closed before authentication finished")
case strings.Contains(errMsg, "convert prelogin-cookie"):
return fmt.Errorf("GlobalProtect VPN authentication succeeded but cookie exchange failed: %w", err)
default:
return fmt.Errorf("GlobalProtect SAML authentication failed: %w", err)
}
}
b.cachedGPSamlMu.Lock()
b.cachedGPSamlCookie = &cachedGPSamlCookie{
ConnectionUUID: targetUUID,
Cookie: authResult.Cookie,
Host: authResult.Host,
User: authResult.User,
Fingerprint: authResult.Fingerprint,
}
b.cachedGPSamlMu.Unlock()
if err := targetConn.ClearSecrets(); err != nil {
log.Warnf("[ConnectVPN] ClearSecrets failed (non-fatal): %v", err)
} else {
log.Infof("[ConnectVPN] Cleared stale stored secrets for %s", connName)
}
log.Infof("[ConnectVPN] GlobalProtect SAML cookie cached for %s, proceeding with activation", connName)
} }
b.stateMutex.Lock() b.stateMutex.Lock()
@@ -384,16 +339,6 @@ func detectVPNAuthAction(serviceType string, data map[string]string) string {
} }
switch { switch {
case strings.Contains(serviceType, "openconnect"):
protocol := data["protocol"]
if needsExternalBrowserAuth(protocol, data["authtype"], data["username"], data) {
switch protocol {
case "gp":
return "gp_saml"
default:
log.Infof("[VPN] External browser auth detected for protocol '%s' but only GlobalProtect (gp) is currently supported", protocol)
}
}
case strings.Contains(serviceType, "openvpn"): case strings.Contains(serviceType, "openvpn"):
connType := data["connection-type"] connType := data["connection-type"]
username := data["username"] username := data["username"]
@@ -725,13 +670,10 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
b.state.LastError = "" b.state.LastError = ""
b.stateMutex.Unlock() b.stateMutex.Unlock()
// Clear cached PKCS11 PIN and SAML cookie on success // Clear cached PKCS11 PIN on success
b.cachedPKCS11Mu.Lock() b.cachedPKCS11Mu.Lock()
b.cachedPKCS11PIN = nil b.cachedPKCS11PIN = nil
b.cachedPKCS11Mu.Unlock() b.cachedPKCS11Mu.Unlock()
b.cachedGPSamlMu.Lock()
b.cachedGPSamlCookie = nil
b.cachedGPSamlMu.Unlock()
b.pendingVPNSaveMu.Lock() b.pendingVPNSaveMu.Lock()
pending := b.pendingVPNSave pending := b.pendingVPNSave
@@ -750,13 +692,10 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
b.state.LastError = "VPN connection failed" b.state.LastError = "VPN connection failed"
b.stateMutex.Unlock() b.stateMutex.Unlock()
// Clear cached PKCS11 PIN and SAML cookie on failure // Clear cached PKCS11 PIN on failure
b.cachedPKCS11Mu.Lock() b.cachedPKCS11Mu.Lock()
b.cachedPKCS11PIN = nil b.cachedPKCS11PIN = nil
b.cachedPKCS11Mu.Unlock() b.cachedPKCS11Mu.Unlock()
b.cachedGPSamlMu.Lock()
b.cachedGPSamlCookie = nil
b.cachedGPSamlMu.Unlock()
return return
} }
} }
@@ -770,13 +709,10 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
b.state.LastError = "VPN connection failed" b.state.LastError = "VPN connection failed"
b.stateMutex.Unlock() b.stateMutex.Unlock()
// Clear cached PKCS11 PIN and SAML cookie // Clear cached PKCS11 PIN
b.cachedPKCS11Mu.Lock() b.cachedPKCS11Mu.Lock()
b.cachedPKCS11PIN = nil b.cachedPKCS11PIN = nil
b.cachedPKCS11Mu.Unlock() b.cachedPKCS11Mu.Unlock()
b.cachedGPSamlMu.Lock()
b.cachedGPSamlCookie = nil
b.cachedGPSamlMu.Unlock()
} }
} }
+2 -9
View File
@@ -1,7 +1,6 @@
package network package network
import ( import (
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -29,13 +28,7 @@ func TestDetectResult_HasNetworkdField(t *testing.T) {
func TestDetectNetworkStack_Integration(t *testing.T) { func TestDetectNetworkStack_Integration(t *testing.T) {
result, err := DetectNetworkStack() result, err := DetectNetworkStack()
if err != nil && strings.Contains(err.Error(), "connect system bus") {
t.Skipf("system D-Bus unavailable: %v", err)
}
assert.NoError(t, err) assert.NoError(t, err)
if assert.NotNil(t, result) { assert.NotNil(t, result)
assert.NotEmpty(t, result.ChosenReason) assert.NotEmpty(t, result.ChosenReason)
}
} }
-37
View File
@@ -1516,11 +1516,7 @@ func Start(printDocs bool) error {
} }
}() }()
loginctlReady := make(chan struct{})
freedesktopReady := make(chan struct{})
go func() { go func() {
defer close(loginctlReady)
if err := InitializeLoginctlManager(); err != nil { if err := InitializeLoginctlManager(); err != nil {
log.Warnf("Loginctl manager unavailable: %v", err) log.Warnf("Loginctl manager unavailable: %v", err)
} else { } else {
@@ -1529,7 +1525,6 @@ func Start(printDocs bool) error {
}() }()
go func() { go func() {
defer close(freedesktopReady)
if err := InitializeFreedeskManager(); err != nil { if err := InitializeFreedeskManager(); err != nil {
log.Warnf("Freedesktop manager unavailable: %v", err) log.Warnf("Freedesktop manager unavailable: %v", err)
} else if freedesktopManager != nil { } else if freedesktopManager != nil {
@@ -1538,31 +1533,6 @@ func Start(printDocs bool) error {
} }
}() }()
// Bridge loginctl lock state to the freedesktop/gnome screensaver
// ActiveChanged signal so apps like Bitwarden can detect screen lock.
go func() {
<-loginctlReady
<-freedesktopReady
if loginctlManager == nil || freedesktopManager == nil {
return
}
ch := loginctlManager.Subscribe("dms-lock-bridge")
defer loginctlManager.Unsubscribe("dms-lock-bridge")
initial := loginctlManager.GetState()
lastLocked := initial.Locked
freedesktopManager.SetScreenLockActive(lastLocked)
for state := range ch {
if state.Locked != lastLocked {
lastLocked = state.Locked
freedesktopManager.SetScreenLockActive(lastLocked)
}
}
}()
if err := InitializeWaylandManager(); err != nil { if err := InitializeWaylandManager(); err != nil {
log.Warnf("Wayland manager unavailable: %v", err) log.Warnf("Wayland manager unavailable: %v", err)
} }
@@ -1599,13 +1569,6 @@ func Start(printDocs bool) error {
log.Warnf("Theme mode manager unavailable: %v", err) log.Warnf("Theme mode manager unavailable: %v", err)
} else { } else {
notifyCapabilityChange() notifyCapabilityChange()
go func() {
<-loginctlReady
if loginctlManager == nil {
return
}
themeModeManager.WatchLoginctl(loginctlManager)
}()
} }
fatalErrChan := make(chan error, 1) fatalErrChan := make(chan error, 1)
+1 -1
View File
@@ -162,7 +162,7 @@ func TestCleanupStaleSockets(t *testing.T) {
tempDir := t.TempDir() tempDir := t.TempDir()
t.Setenv("XDG_RUNTIME_DIR", tempDir) t.Setenv("XDG_RUNTIME_DIR", tempDir)
staleSocket := filepath.Join(tempDir, "danklinux-4194305.sock") staleSocket := filepath.Join(tempDir, "danklinux-999999.sock")
err := os.WriteFile(staleSocket, []byte{}, 0o600) err := os.WriteFile(staleSocket, []byte{}, 0o600)
require.NoError(t, err) require.NoError(t, err)
+17 -37
View File
@@ -5,7 +5,6 @@ import (
"sync" "sync"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
@@ -188,29 +187,6 @@ func (m *Manager) Close() {
}) })
} }
func (m *Manager) WatchLoginctl(lm *loginctl.Manager) {
ch := lm.Subscribe("thememode")
m.wg.Add(1)
go func() {
defer m.wg.Done()
defer lm.Unsubscribe("thememode")
for {
select {
case <-m.stopChan:
return
case state, ok := <-ch:
if !ok {
return
}
if state.PreparingForSleep {
continue
}
m.TriggerUpdate()
}
}
}()
}
func (m *Manager) schedulerLoop() { func (m *Manager) schedulerLoop() {
defer m.wg.Done() defer m.wg.Done()
@@ -351,12 +327,10 @@ func statesEqual(a, b *State) bool {
} }
func (m *Manager) computeSchedule(now time.Time, config Config) (bool, time.Time) { func (m *Manager) computeSchedule(now time.Time, config Config) (bool, time.Time) {
switch config.Mode { if config.Mode == "location" {
case "location":
return m.computeLocationSchedule(now, config) return m.computeLocationSchedule(now, config)
default:
return computeTimeSchedule(now, config)
} }
return computeTimeSchedule(now, config)
} }
func computeTimeSchedule(now time.Time, config Config) (bool, time.Time) { func computeTimeSchedule(now time.Time, config Config) (bool, time.Time) {
@@ -407,10 +381,10 @@ func (m *Manager) computeLocationSchedule(now time.Time, config Config) (bool, t
} }
times, cond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, now, config.ElevationTwilight, config.ElevationDaylight) times, cond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, now, config.ElevationTwilight, config.ElevationDaylight)
switch cond { if cond != wayland.SunNormal {
case wayland.SunMidnightSun: if cond == wayland.SunMidnightSun {
return true, startOfNextDay(now) return true, startOfNextDay(now)
case wayland.SunPolarNight: }
return false, startOfNextDay(now) return false, startOfNextDay(now)
} }
@@ -423,10 +397,10 @@ func (m *Manager) computeLocationSchedule(now time.Time, config Config) (bool, t
nextDay := startOfNextDay(now) nextDay := startOfNextDay(now)
nextTimes, nextCond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, nextDay, config.ElevationTwilight, config.ElevationDaylight) nextTimes, nextCond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, nextDay, config.ElevationTwilight, config.ElevationDaylight)
switch nextCond { if nextCond != wayland.SunNormal {
case wayland.SunMidnightSun: if nextCond == wayland.SunMidnightSun {
return true, startOfNextDay(nextDay) return true, startOfNextDay(nextDay)
case wayland.SunPolarNight: }
return false, startOfNextDay(nextDay) return false, startOfNextDay(nextDay)
} }
@@ -439,7 +413,13 @@ func startOfNextDay(t time.Time) time.Time {
} }
func validateHourMinute(hour, minute int) bool { func validateHourMinute(hour, minute int) bool {
return hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59 if hour < 0 || hour > 23 {
return false
}
if minute < 0 || minute > 59 {
return false
}
return true
} }
func (m *Manager) ValidateSchedule(startHour, startMinute, endHour, endMinute int) error { func (m *Manager) ValidateSchedule(startHour, startMinute, endHour, endMinute int) error {
+4 -17
View File
@@ -2,10 +2,10 @@ package wlcontext
import ( import (
"fmt" "fmt"
"golang.org/x/sys/unix"
"os" "os"
"sync" "sync"
"time"
"golang.org/x/sys/unix"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -123,9 +123,6 @@ func (sc *SharedContext) eventDispatcher() {
{Fd: int32(sc.wakeR), Events: unix.POLLIN}, {Fd: int32(sc.wakeR), Events: unix.POLLIN},
} }
consecutiveErrors := 0
const maxConsecutiveErrors = 20
for { for {
sc.drainCmdQueue() sc.drainCmdQueue()
@@ -156,19 +153,9 @@ func (sc *SharedContext) eventDispatcher() {
} }
if err := ctx.Dispatch(); err != nil && !os.IsTimeout(err) { if err := ctx.Dispatch(); err != nil && !os.IsTimeout(err) {
consecutiveErrors++ log.Errorf("Wayland connection error: %v", err)
log.Warnf("Wayland connection error (%d/%d): %v", consecutiveErrors, maxConsecutiveErrors, err) return
if consecutiveErrors >= maxConsecutiveErrors {
log.Errorf("Fatal: Wayland connection unrecoverable after %d attempts. Exiting dispatcher.", maxConsecutiveErrors)
return
}
time.Sleep(100 * time.Millisecond * time.Duration(consecutiveErrors))
continue
} }
consecutiveErrors = 0
} }
} }
+2 -50
View File
@@ -7,7 +7,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
@@ -81,24 +80,19 @@ func (m Model) viewDependencyReview() string {
} }
} }
note := ""
if dep.Name == "dms-greeter" {
note = m.styles.Subtle.Render(" (selection replaces your current display manager)")
}
var line string var line string
if i == m.selectedDep { if i == m.selectedDep {
line = fmt.Sprintf("▶ %s%s%-25s %s", reinstallMarker, variantMarker, dep.Name, status) line = fmt.Sprintf("▶ %s%s%-25s %s", reinstallMarker, variantMarker, dep.Name, status)
if dep.Version != "" { if dep.Version != "" {
line += fmt.Sprintf(" (%s)", dep.Version) line += fmt.Sprintf(" (%s)", dep.Version)
} }
line = m.styles.SelectedOption.Render(line) + note line = m.styles.SelectedOption.Render(line)
} else { } else {
line = fmt.Sprintf(" %s%s%-25s %s", reinstallMarker, variantMarker, dep.Name, status) line = fmt.Sprintf(" %s%s%-25s %s", reinstallMarker, variantMarker, dep.Name, status)
if dep.Version != "" { if dep.Version != "" {
line += fmt.Sprintf(" (%s)", dep.Version) line += fmt.Sprintf(" (%s)", dep.Version)
} }
line = m.styles.Normal.Render(line) + note line = m.styles.Normal.Render(line)
} }
b.WriteString(line) b.WriteString(line)
@@ -121,13 +115,6 @@ func (m Model) updateDetectingDepsState(msg tea.Msg) (tea.Model, tea.Cmd) {
m.state = StateError m.state = StateError
} else { } else {
m.dependencies = depsMsg.deps m.dependencies = depsMsg.deps
// dms-greeter is opt-in skipped by default
for _, dep := range depsMsg.deps {
if dep.Name == "dms-greeter" {
m.disabledItems["dms-greeter"] = true
break
}
}
m.state = StateDependencyReview m.state = StateDependencyReview
} }
return m, m.listenForLogs() return m, m.listenForLogs()
@@ -243,41 +230,6 @@ func (m Model) installPackages() tea.Cmd {
// Convert installer messages to TUI messages // Convert installer messages to TUI messages
go func() { go func() {
for msg := range installerProgressChan { for msg := range installerProgressChan {
// Run optional greeter setup
if msg.Phase == distros.PhaseComplete && msg.IsComplete && msg.Error == nil {
greeterSelected := false
for _, dep := range m.dependencies {
if dep.Name == "dms-greeter" && !m.disabledItems["dms-greeter"] {
greeterSelected = true
break
}
}
if greeterSelected {
compositorName := "niri"
if m.selectedWM == 1 {
compositorName = "Hyprland"
}
m.packageProgressChan <- packageInstallProgressMsg{
progress: 0.92,
step: "Configuring DMS greeter...",
logOutput: "Starting automated greeter setup...",
}
greeterLogFunc := func(line string) {
m.packageProgressChan <- packageInstallProgressMsg{
progress: 0.94,
step: "Configuring DMS greeter...",
logOutput: line,
}
}
if err := greeter.AutoSetupGreeter(compositorName, m.sudoPassword, greeterLogFunc); err != nil {
m.packageProgressChan <- packageInstallProgressMsg{
progress: 0.96,
step: "Greeter setup warning",
logOutput: fmt.Sprintf("⚠ Greeter auto-setup warning (non-fatal): %v", err),
}
}
}
}
tuiMsg := packageInstallProgressMsg{ tuiMsg := packageInstallProgressMsg{
progress: msg.Progress, progress: msg.Progress,
step: msg.Step, step: msg.Step,
-16
View File
@@ -38,22 +38,6 @@ func XDGConfigHome() string {
return filepath.Join(home, ".config") 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) { func ExpandPath(path string) (string, error) {
expanded := os.ExpandEnv(path) expanded := os.ExpandEnv(path)
expanded = filepath.Clean(expanded) expanded = filepath.Clean(expanded)
@@ -864,12 +864,10 @@ func (p *NiriWritableProvider) formatRule(rule windowrules.WindowRule) string {
func formatSizeProperty(name, value string) string { func formatSizeProperty(name, value string) string {
parts := strings.SplitN(value, " ", 2) parts := strings.SplitN(value, " ", 2)
if len(parts) == 2 { if len(parts) != 2 {
return fmt.Sprintf(" %s { %s %s; }", name, parts[0], parts[1]) return fmt.Sprintf(" %s { }", name)
} }
// Bare number without type prefix — default to "fixed" sizeType := parts[0]
if _, err := strconv.Atoi(value); err == nil { sizeValue := parts[1]
return fmt.Sprintf(" %s { fixed %s; }", name, value) return fmt.Sprintf(" %s { %s %s; }", name, sizeType, sizeValue)
}
return fmt.Sprintf(" %s { }", name)
} }
-1
View File
@@ -29,7 +29,6 @@ Depends: ${misc:Depends},
qml6-module-qtquick-templates, qml6-module-qtquick-templates,
qml6-module-qtquick-window, qml6-module-qtquick-window,
qt6ct qt6ct
Suggests: cups-pk-helper
Provides: dms Provides: dms
Conflicts: dms Conflicts: dms
Replaces: dms Replaces: dms
-9
View File
@@ -1,9 +0,0 @@
<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>
@@ -1,6 +0,0 @@
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
View File
@@ -1,23 +0,0 @@
Source: dms-greeter
Section: x11
Priority: optional
Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
Build-Depends: debhelper-compat (= 13)
Standards-Version: 4.6.2
Homepage: https://github.com/AvengeMedia/DankMaterialShell
Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell
Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
Package: dms-greeter
Architecture: any
Depends: ${misc:Depends},
greetd,
quickshell-git | quickshell
Suggests: niri | hyprland | sway
Description: DankMaterialShell greeter for greetd
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
inspired greeter interface built with Quickshell for Wayland compositors.
.
Supports multiple compositors including Niri, Hyprland, and Sway with automatic
compositor detection and configuration. Features session selection, user
authentication, and dynamic theming.
@@ -1,27 +0,0 @@
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
View File
@@ -1,108 +0,0 @@
#!/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
View File
@@ -1,14 +0,0 @@
#!/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
View File
@@ -1,48 +0,0 @@
#!/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 +0,0 @@
3.0 (native)
@@ -1 +0,0 @@
# OBS _service downloads dms-qml.tar.gz; no extra excludes needed
+1 -2
View File
@@ -9,7 +9,7 @@ Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell
Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
Package: dms Package: dms
Architecture: amd64 arm64 Architecture: amd64
Depends: ${misc:Depends}, Depends: ${misc:Depends},
quickshell | quickshell-git, quickshell | quickshell-git,
accountsservice, accountsservice,
@@ -28,7 +28,6 @@ Depends: ${misc:Depends},
qml6-module-qtquick-templates, qml6-module-qtquick-templates,
qml6-module-qtquick-window, qml6-module-qtquick-window,
qt6ct qt6ct
Suggests: cups-pk-helper
Conflicts: dms-git Conflicts: dms-git
Replaces: dms-git Replaces: dms-git
Description: DankMaterialShell - Modern Wayland Desktop Shell Description: DankMaterialShell - Modern Wayland Desktop Shell
@@ -1,3 +1,2 @@
dms-distropkg-amd64.gz dms-distropkg-amd64.gz
dms-distropkg-arm64.gz
dms-source.tar.gz dms-source.tar.gz
-1
View File
@@ -1,5 +1,4 @@
# Include files that are normally excluded by .gitignore # Include files that are normally excluded by .gitignore
# These are needed for the build process on Launchpad # These are needed for the build process on Launchpad
tar-ignore = !dms-distropkg-amd64.gz tar-ignore = !dms-distropkg-amd64.gz
tar-ignore = !dms-distropkg-arm64.gz
tar-ignore = !dms-source.tar.gz tar-ignore = !dms-source.tar.gz
-1
View File
@@ -37,7 +37,6 @@ Recommends: quickshell-git
# Recommended system packages # Recommended system packages
Recommends: NetworkManager Recommends: NetworkManager
Recommends: qt6-qtmultimedia Recommends: qt6-qtmultimedia
Suggests: cups-pk-helper
Suggests: qt6ct Suggests: qt6ct
%description %description
-1
View File
@@ -28,7 +28,6 @@ Recommends: danksearch
Recommends: matugen Recommends: matugen
Recommends: NetworkManager Recommends: NetworkManager
Recommends: qt6-qtmultimedia Recommends: qt6-qtmultimedia
Suggests: cups-pk-helper
Suggests: qt6ct Suggests: qt6ct
%description %description
+1 -5
View File
@@ -23,7 +23,6 @@ let
lib.makeBinPath [ lib.makeBinPath [
cfg.quickshell.package cfg.quickshell.package
compositorPackage compositorPackage
pkgs.glib # provides gdbus, used by the fprintd hardware probe in GreeterContent.qml
] ]
} }
${ ${
@@ -74,7 +73,6 @@ in
"labwc" "labwc"
"mango" "mango"
"scroll" "scroll"
"miracle"
]; ];
description = "Compositor to run greeter in"; description = "Compositor to run greeter in";
}; };
@@ -180,9 +178,7 @@ in
fi fi
if [ -f settings.json ]; then if [ -f settings.json ]; then
theme_file="$(${jq} -r '.customThemeFile // empty' settings.json)" if cp "$(${jq} -r '.customThemeFile' settings.json)" custom-theme.json; then
if [ -f "$theme_file" ] && [ -r "$theme_file" ]; then
cp "$theme_file" custom-theme.json
mv settings.json settings.orig.json mv settings.json settings.orig.json
${jq} '.customThemeFile = "${cacheDir}/custom-theme.json"' settings.orig.json > settings.json ${jq} '.customThemeFile = "${cacheDir}/custom-theme.json"' settings.orig.json > settings.json
fi fi
-1
View File
@@ -50,6 +50,5 @@ in
services.power-profiles-daemon.enable = lib.mkDefault true; services.power-profiles-daemon.enable = lib.mkDefault true;
services.accounts-daemon.enable = lib.mkDefault true; services.accounts-daemon.enable = lib.mkDefault true;
security.polkit.enable = lib.mkDefault true;
}; };
} }
-1
View File
@@ -25,7 +25,6 @@ Recommends: matugen
Recommends: quickshell-git Recommends: quickshell-git
Recommends: NetworkManager Recommends: NetworkManager
Recommends: qt6-qtmultimedia Recommends: qt6-qtmultimedia
Suggests: cups-pk-helper
Suggests: qt6ct Suggests: qt6ct
Provides: dms Provides: dms
-322
View File
@@ -1,322 +0,0 @@
# 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
-1
View File
@@ -27,7 +27,6 @@ Recommends: danksearch
Recommends: matugen Recommends: matugen
Recommends: NetworkManager Recommends: NetworkManager
Recommends: qt6-qtmultimedia Recommends: qt6-qtmultimedia
Suggests: cups-pk-helper
Suggests: qt6ct Suggests: qt6ct
%description %description

Some files were not shown because too many files have changed in this diff Show More