mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-02 10:32:07 -04:00
Compare commits
58 Commits
754bf8fa3c
...
v1.4.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03a8e1e0d5 | ||
|
|
4d4d3c20a1 | ||
|
|
cef16d6bc9 | ||
|
|
aafaad1791 | ||
|
|
7906fdc2b0 | ||
|
|
397650ca52 | ||
|
|
826207006a | ||
|
|
58c2fcd31c | ||
|
|
b2a2b425ec | ||
|
|
942c9c9609 | ||
|
|
46d6e1cff3 | ||
|
|
a4137c57c1 | ||
|
|
1ad8b627f1 | ||
|
|
58a02ce290 | ||
|
|
8e1ad1a2be | ||
|
|
68cd7ab32c | ||
|
|
f649ce9a8e | ||
|
|
c4df242f07 | ||
|
|
26846c8d55 | ||
|
|
31b44a667c | ||
|
|
4f3b73ee21 | ||
|
|
4cfae91f02 | ||
|
|
8d947a6e95 | ||
|
|
1e84d4252c | ||
|
|
76072e1d4c | ||
|
|
6408dce4a9 | ||
|
|
0b2e1cca38 | ||
|
|
c1bfd8c0b7 | ||
|
|
90ffa5833b | ||
|
|
169c669286 | ||
|
|
f8350deafc | ||
|
|
0286a1b80b | ||
|
|
7c3e6c1f02 | ||
|
|
d2d72db3c9 | ||
|
|
f81f861408 | ||
|
|
af494543f5 | ||
|
|
db4de55338 | ||
|
|
37ecbbbbde | ||
|
|
d6a6d2a438 | ||
|
|
bf1c6eec74 | ||
|
|
0ddae80584 | ||
|
|
5c96c03bfa | ||
|
|
dfe36e47d8 | ||
|
|
63e1b75e57 | ||
|
|
29efdd8598 | ||
|
|
34d03cf11b | ||
|
|
c339389d44 | ||
|
|
af5f6eb656 | ||
|
|
a6d28e2553 | ||
|
|
6213267908 | ||
|
|
d084114149 | ||
|
|
f6d99eca0d | ||
|
|
722eb3289e | ||
|
|
b7f2bdcb2d | ||
|
|
11c20db6e6 | ||
|
|
8a4e3f8bb1 | ||
|
|
bc8fe97c13 | ||
|
|
47262155aa |
8
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
8
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -7,7 +7,7 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
## 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
|
||||
id: compositor
|
||||
attributes:
|
||||
@@ -53,9 +53,9 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: original_installation_method
|
||||
id: original_installation_method_different
|
||||
attributes:
|
||||
label: Was this your original Installation method?
|
||||
label: Was your original Installation method different?
|
||||
options:
|
||||
- "Yes"
|
||||
- No (specify below)
|
||||
@@ -73,7 +73,7 @@ body:
|
||||
id: dms_doctor
|
||||
attributes:
|
||||
label: dms doctor -vC
|
||||
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 -vC` command — paste between the lines below to keep it collapsed in the issue
|
||||
placeholder: Paste the output of `dms doctor -vC` here
|
||||
value: |
|
||||
<details>
|
||||
|
||||
256
.github/workflows/run-obs.yml
vendored
256
.github/workflows/run-obs.yml
vendored
@@ -9,7 +9,6 @@ on:
|
||||
type: choice
|
||||
options:
|
||||
- dms
|
||||
- dms-greeter
|
||||
- dms-git
|
||||
- all
|
||||
default: "dms"
|
||||
@@ -73,27 +72,12 @@ jobs:
|
||||
fi
|
||||
}
|
||||
|
||||
# Helper function to check dms-greeter stable tag
|
||||
check_dms_greeter_stable() {
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:danklinux/dms-greeter/dms-greeter.spec" 2>/dev/null || echo "")
|
||||
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs | sed 's/^v//')
|
||||
|
||||
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "v$OBS_VERSION" ]]; then
|
||||
echo "📋 dms-greeter: Tag $LATEST_TAG already exists, skipping"
|
||||
return 1 # No update needed
|
||||
else
|
||||
echo "📋 dms-greeter: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
|
||||
return 0 # Update needed
|
||||
fi
|
||||
}
|
||||
|
||||
# Main logic
|
||||
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then
|
||||
# Run from tag with no package specified - update both stable packages
|
||||
echo "packages=dms dms-greeter" >> $GITHUB_OUTPUT
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
# Tag selected or pushed - always update stable package
|
||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
@@ -119,18 +103,15 @@ jobs:
|
||||
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
|
||||
|
||||
elif [[ "$PKG" == "all" ]]; then
|
||||
# Check each stable package and build list of those needing updates
|
||||
# Check each package and build list of those needing updates
|
||||
PACKAGES_TO_UPDATE=()
|
||||
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
|
||||
if check_dms_stable; then
|
||||
PACKAGES_TO_UPDATE+=("dms")
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
if check_dms_greeter_stable; then
|
||||
PACKAGES_TO_UPDATE+=("dms-greeter")
|
||||
[[ -n "$LATEST_TAG" ]] && echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
||||
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
||||
@@ -139,7 +120,7 @@ jobs:
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
echo "✓ Both packages up to date"
|
||||
echo "✓ All packages up to date"
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms-git" ]]; then
|
||||
@@ -163,18 +144,6 @@ jobs:
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms-greeter" ]]; then
|
||||
if check_dms_greeter_stable; then
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
else
|
||||
# Unknown package - proceed anyway
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
@@ -202,8 +171,15 @@ jobs:
|
||||
- name: Determine packages to update
|
||||
id: packages
|
||||
run: |
|
||||
# Use check-updates outputs when available
|
||||
if [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
|
||||
# Check if GITHUB_REF points to a tag (works for both push events and workflow_dispatch with tag selected)
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
# Tag selected or pushed - use the tag from GITHUB_REF
|
||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Using tag from GITHUB_REF: $VERSION"
|
||||
# Check if check-updates already determined a version (from auto-detection)
|
||||
elif [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
|
||||
# Use version from check-updates job
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
|
||||
@@ -215,16 +191,40 @@ jobs:
|
||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||
# Manual workflow dispatch
|
||||
|
||||
# Determine version for dms stable and dms-greeter using the API
|
||||
# GITHUB_REF is unreliable when "Use workflow from" a tag; API works from any ref
|
||||
if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "dms-greeter" ]] || [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
echo "Using latest release from API: $LATEST_TAG"
|
||||
# Determine version for dms stable
|
||||
if [[ "${{ github.event.inputs.package }}" == "dms" ]]; then
|
||||
# Use github.ref if tag selected, otherwise auto-detect latest
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Using tag from GITHUB_REF: $VERSION"
|
||||
else
|
||||
echo "ERROR: Could not fetch latest release from API"
|
||||
exit 1
|
||||
# Auto-detect latest release for dms
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
echo "Auto-detected latest release: $LATEST_TAG"
|
||||
else
|
||||
echo "ERROR: Could not auto-detect latest release"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
elif [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
||||
# Use github.ref if tag selected, otherwise auto-detect latest
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Using tag from GITHUB_REF: $VERSION"
|
||||
else
|
||||
# Auto-detect latest release for "all"
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
echo "Auto-detected latest release: $LATEST_TAG"
|
||||
else
|
||||
echo "ERROR: Could not auto-detect latest release"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -244,7 +244,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Update dms-git spec version
|
||||
if: contains(steps.packages.outputs.packages, 'dms-git')
|
||||
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
||||
run: |
|
||||
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
@@ -265,7 +265,7 @@ jobs:
|
||||
} > distro/opensuse/dms-git.spec
|
||||
|
||||
- name: Update Debian dms-git changelog version
|
||||
if: contains(steps.packages.outputs.packages, 'dms-git')
|
||||
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
||||
run: |
|
||||
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
@@ -283,66 +283,55 @@ jobs:
|
||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
||||
} > "distro/debian/dms-git/debian/changelog"
|
||||
|
||||
- name: Update stable version (dms + dms-greeter)
|
||||
- name: Update dms stable version
|
||||
if: steps.packages.outputs.version != ''
|
||||
run: |
|
||||
VERSION="${{ steps.packages.outputs.version }}"
|
||||
VERSION_NO_V="${VERSION#v}"
|
||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||
echo "==> Updating packaging files to version: $VERSION_NO_V"
|
||||
|
||||
# Update dms spec and changelog when dms is in the upload list
|
||||
if [[ "$PACKAGES" == *"dms"* ]]; then
|
||||
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
|
||||
UPDATED_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1)
|
||||
echo "✓ dms spec now shows Version: $UPDATED_VERSION"
|
||||
# Update spec file
|
||||
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
|
||||
|
||||
DATE_STR=$(date "+%a %b %d %Y")
|
||||
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms.spec)
|
||||
{
|
||||
echo "$LOCAL_SPEC_HEAD"
|
||||
echo "%changelog"
|
||||
echo "* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1"
|
||||
echo "- Update to stable $VERSION release"
|
||||
} > distro/opensuse/dms.spec
|
||||
# Verify the update
|
||||
UPDATED_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1)
|
||||
echo "✓ Spec file now shows Version: $UPDATED_VERSION"
|
||||
|
||||
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
|
||||
CHANGELOG_DATE=$(date -R)
|
||||
{
|
||||
echo "dms (${VERSION_NO_V}db1) stable; urgency=medium"
|
||||
echo ""
|
||||
echo " * Update to $VERSION stable release"
|
||||
echo ""
|
||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
||||
} > "distro/debian/dms/debian/changelog"
|
||||
echo "✓ Updated dms changelog to ${VERSION_NO_V}db1"
|
||||
fi
|
||||
fi
|
||||
# Single changelog entry (full history on OBS website)
|
||||
DATE_STR=$(date "+%a %b %d %Y")
|
||||
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms.spec)
|
||||
{
|
||||
echo "$LOCAL_SPEC_HEAD"
|
||||
echo "%changelog"
|
||||
echo "* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1"
|
||||
echo "- Update to stable $VERSION release"
|
||||
} > distro/opensuse/dms.spec
|
||||
|
||||
# Update dms-greeter changelog when dms-greeter is in the upload list
|
||||
if [[ "$PACKAGES" == *"dms-greeter"* ]] && [[ -f "distro/debian/dms-greeter/debian/changelog" ]]; then
|
||||
CHANGELOG_DATE=$(date -R)
|
||||
{
|
||||
echo "dms-greeter (${VERSION_NO_V}db1) unstable; urgency=medium"
|
||||
echo ""
|
||||
echo " * Update to $VERSION stable release"
|
||||
echo ""
|
||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
||||
} > "distro/debian/dms-greeter/debian/changelog"
|
||||
echo "✓ Updated dms-greeter changelog to ${VERSION_NO_V}db1"
|
||||
fi
|
||||
|
||||
# Update Debian _service files for packages in upload list (download_url paths)
|
||||
# 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, dms-greeter stable)
|
||||
|
||||
# Update download_url paths (for dms stable)
|
||||
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
|
||||
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
|
||||
fi
|
||||
done
|
||||
|
||||
# Update Debian changelog for dms stable (single entry, history on OBS website)
|
||||
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
|
||||
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
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
@@ -365,7 +354,6 @@ jobs:
|
||||
chmod 600 ~/.config/osc/oscrc
|
||||
|
||||
- name: Upload to OBS
|
||||
id: upload
|
||||
env:
|
||||
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
||||
TAG_VERSION: ${{ steps.packages.outputs.version }}
|
||||
@@ -374,8 +362,6 @@ jobs:
|
||||
|
||||
if [[ -z "$PACKAGES" ]]; then
|
||||
echo "✓ No packages need uploading. All up to date!"
|
||||
echo "uploaded_packages=" >> $GITHUB_OUTPUT
|
||||
echo "skipped_packages=" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -385,10 +371,7 @@ jobs:
|
||||
echo "==> Version being uploaded: ${{ steps.packages.outputs.version }}"
|
||||
fi
|
||||
|
||||
UPLOADED_PACKAGES=()
|
||||
SKIPPED_PACKAGES=()
|
||||
|
||||
# PACKAGES can be space-separated list (e.g., "dms dms-greeter" from "all" check)
|
||||
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
|
||||
# Loop through each package and upload
|
||||
for PKG in $PACKAGES; do
|
||||
echo ""
|
||||
@@ -399,37 +382,13 @@ jobs:
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
LOG_FILE=$(mktemp)
|
||||
set +e
|
||||
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
|
||||
bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE" >"$LOG_FILE" 2>&1
|
||||
bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE"
|
||||
fi
|
||||
STATUS=$?
|
||||
set -e
|
||||
|
||||
cat "$LOG_FILE"
|
||||
|
||||
if [[ $STATUS -ne 0 ]]; then
|
||||
rm -f "$LOG_FILE"
|
||||
echo "❌ Upload failed for $PKG"
|
||||
exit $STATUS
|
||||
fi
|
||||
|
||||
if grep -Eq "Exiting gracefully \(no changes needed\)|No changes needed for this package\. Exiting gracefully\." "$LOG_FILE"; then
|
||||
echo "ℹ️ $PKG is already up to date. Skipped."
|
||||
SKIPPED_PACKAGES+=("$PKG")
|
||||
else
|
||||
UPLOADED_PACKAGES+=("$PKG")
|
||||
fi
|
||||
|
||||
rm -f "$LOG_FILE"
|
||||
done
|
||||
|
||||
echo "uploaded_packages=${UPLOADED_PACKAGES[*]}" >> $GITHUB_OUTPUT
|
||||
echo "skipped_packages=${SKIPPED_PACKAGES[*]}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
@@ -443,59 +402,20 @@ jobs:
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "All packages are current. Run completed successfully." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
UPLOADED_PACKAGES="${{ steps.upload.outputs.uploaded_packages }}"
|
||||
SKIPPED_PACKAGES="${{ steps.upload.outputs.skipped_packages }}"
|
||||
TOTAL_COUNT=$(wc -w <<<"$PACKAGES" | tr -d ' ')
|
||||
UPLOADED_COUNT=0
|
||||
SKIPPED_COUNT=0
|
||||
if [[ -n "$UPLOADED_PACKAGES" ]]; then
|
||||
UPLOADED_COUNT=$(wc -w <<<"$UPLOADED_PACKAGES" | tr -d ' ')
|
||||
fi
|
||||
if [[ -n "$SKIPPED_PACKAGES" ]]; then
|
||||
SKIPPED_COUNT=$(wc -w <<<"$SKIPPED_PACKAGES" | tr -d ' ')
|
||||
fi
|
||||
in_list() {
|
||||
local item="$1"
|
||||
local list="$2"
|
||||
[[ " $list " == *" $item "* ]]
|
||||
}
|
||||
|
||||
if [[ "${{ job.status }}" == "success" ]]; then
|
||||
echo "**Status:** ✅ Completed successfully" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "**Status:** ❌ Completed with errors" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Processed:** $TOTAL_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Uploaded:** $UPLOADED_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Skipped (up to date):** $SKIPPED_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Packages:**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Packages Uploaded:**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
for PKG in $PACKAGES; do
|
||||
STATUS_ICON="✅"
|
||||
STATUS_TEXT="uploaded"
|
||||
if in_list "$PKG" "$SKIPPED_PACKAGES"; then
|
||||
STATUS_ICON="ℹ️"
|
||||
STATUS_TEXT="up to date (skipped)"
|
||||
elif ! in_list "$PKG" "$UPLOADED_PACKAGES"; then
|
||||
STATUS_ICON="❌"
|
||||
STATUS_TEXT="failed"
|
||||
fi
|
||||
|
||||
case "$PKG" in
|
||||
dms)
|
||||
echo "- $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)
|
||||
echo "- $STATUS_ICON **dms-git** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
|
||||
;;
|
||||
dms-greeter)
|
||||
echo "- $STATUS_ICON **dms-greeter** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:danklinux/dms-greeter)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ✅ **dms-git** → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
|
||||
|
||||
16
.github/workflows/run-ppa.yml
vendored
16
.github/workflows/run-ppa.yml
vendored
@@ -4,15 +4,9 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: "Package to upload"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- dms
|
||||
- dms-greeter
|
||||
- dms-git
|
||||
- all
|
||||
default: "dms"
|
||||
description: "Package to upload (dms, dms-git, dms-greeter, or all)"
|
||||
required: false
|
||||
default: "dms-git"
|
||||
rebuild_release:
|
||||
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
|
||||
required: false
|
||||
@@ -145,7 +139,7 @@ jobs:
|
||||
fi
|
||||
else
|
||||
# Fallback
|
||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
@@ -215,7 +209,7 @@ jobs:
|
||||
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||
fi
|
||||
|
||||
# PACKAGES can be space-separated list (e.g., "dms-git dms dms-greeter" from "all" check)
|
||||
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
|
||||
# Loop through each package and upload
|
||||
for PKG in $PACKAGES; do
|
||||
# Map package to PPA name
|
||||
|
||||
@@ -28,12 +28,6 @@ packages:
|
||||
outpkg: mocks_brightness
|
||||
interfaces:
|
||||
DBusConn:
|
||||
github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation:
|
||||
config:
|
||||
dir: "internal/mocks/geolocation"
|
||||
outpkg: mocks_geolocation
|
||||
interfaces:
|
||||
Client:
|
||||
github.com/AvengeMedia/DankMaterialShell/core/internal/server/network:
|
||||
config:
|
||||
dir: "internal/mocks/network"
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"policy_version": 1,
|
||||
"blocked_commands": [
|
||||
"greeter install",
|
||||
"greeter enable",
|
||||
"greeter sync",
|
||||
"setup"
|
||||
],
|
||||
"message": "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes."
|
||||
}
|
||||
@@ -222,19 +222,16 @@ func init() {
|
||||
|
||||
func runClipCopy(cmd *cobra.Command, args []string) {
|
||||
var data []byte
|
||||
copyFromStdin := false
|
||||
|
||||
switch {
|
||||
case len(args) > 0:
|
||||
data = []byte(args[0])
|
||||
case clipCopyDownload || clipCopyType == "__multi__":
|
||||
default:
|
||||
var err error
|
||||
data, err = io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
log.Fatalf("read stdin: %v", err)
|
||||
}
|
||||
default:
|
||||
copyFromStdin = true
|
||||
}
|
||||
|
||||
if clipCopyDownload {
|
||||
@@ -260,13 +257,6 @@ func runClipCopy(cmd *cobra.Command, args []string) {
|
||||
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 {
|
||||
log.Fatalf("copy: %v", err)
|
||||
}
|
||||
|
||||
@@ -525,6 +525,5 @@ func getCommonCommands() []*cobra.Command {
|
||||
doctorCmd,
|
||||
configCmd,
|
||||
dlCmd,
|
||||
randrCmd,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -652,14 +652,16 @@ func checkI2CAvailability() checkResult {
|
||||
func checkImageFormatPlugins() []checkResult {
|
||||
url := doctorDocsURL + "#optional-features"
|
||||
|
||||
pluginDirs := findQtPluginDirs()
|
||||
if len(pluginDirs) == 0 {
|
||||
pluginDir := findQtPluginDir()
|
||||
if pluginDir == "" {
|
||||
return []checkResult{
|
||||
{catOptionalFeatures, "qt6-imageformats", statusInfo, "Cannot detect (plugin dir not found)", "WebP, TIFF, JP2 support", url},
|
||||
{catOptionalFeatures, "kimageformats", statusInfo, "Cannot detect (plugin dir not found)", "AVIF, HEIF, JXL support", url},
|
||||
}
|
||||
}
|
||||
|
||||
imageFormatsDir := filepath.Join(pluginDir, "imageformats")
|
||||
|
||||
type pluginCheck struct {
|
||||
name string
|
||||
desc string
|
||||
@@ -693,18 +695,9 @@ func checkImageFormatPlugins() []checkResult {
|
||||
var results []checkResult
|
||||
for _, c := range checks {
|
||||
var found []string
|
||||
var foundDirs []string
|
||||
for _, pluginDir := range pluginDirs {
|
||||
imageFormatsDir := filepath.Join(pluginDir, "imageformats")
|
||||
for _, p := range c.plugins {
|
||||
if _, err := os.Stat(filepath.Join(imageFormatsDir, p.file)); err == nil {
|
||||
if !slices.Contains(found, p.format) {
|
||||
found = append(found, p.format)
|
||||
}
|
||||
if !slices.Contains(foundDirs, imageFormatsDir) {
|
||||
foundDirs = append(foundDirs, imageFormatsDir)
|
||||
}
|
||||
}
|
||||
for _, p := range c.plugins {
|
||||
if _, err := os.Stat(filepath.Join(imageFormatsDir, p.file)); err == nil {
|
||||
found = append(found, p.format)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -715,7 +708,7 @@ func checkImageFormatPlugins() []checkResult {
|
||||
default:
|
||||
details := ""
|
||||
if doctorVerbose {
|
||||
details = fmt.Sprintf("Formats: %s (%s)", strings.Join(found, ", "), strings.Join(foundDirs, ":"))
|
||||
details = fmt.Sprintf("Formats: %s (%s)", strings.Join(found, ", "), imageFormatsDir)
|
||||
}
|
||||
result = checkResult{catOptionalFeatures, c.name, statusOK, fmt.Sprintf("Installed (%d formats)", len(found)), details, url}
|
||||
}
|
||||
@@ -725,28 +718,22 @@ func checkImageFormatPlugins() []checkResult {
|
||||
return results
|
||||
}
|
||||
|
||||
func findQtPluginDirs() []string {
|
||||
var dirs []string
|
||||
|
||||
addDir := func(dir string) {
|
||||
if dir != "" {
|
||||
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
|
||||
dirs = append(dirs, dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check all paths in QT_PLUGIN_PATH env var (used by NixOS and custom setups)
|
||||
func findQtPluginDir() string {
|
||||
// Check QT_PLUGIN_PATH env var first (used by NixOS and custom setups)
|
||||
if envPath := os.Getenv("QT_PLUGIN_PATH"); envPath != "" {
|
||||
for dir := range strings.SplitSeq(envPath, ":") {
|
||||
addDir(dir)
|
||||
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
|
||||
return dir
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try qtpaths
|
||||
for _, cmd := range []string{"qtpaths6", "qtpaths"} {
|
||||
if output, err := exec.Command(cmd, "-query", "QT_INSTALL_PLUGINS").Output(); err == nil {
|
||||
addDir(strings.TrimSpace(string(output)))
|
||||
if dir := strings.TrimSpace(string(output)); dir != "" {
|
||||
return dir
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -757,10 +744,12 @@ func findQtPluginDirs() []string {
|
||||
"/usr/lib/x86_64-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 {
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
@@ -24,10 +23,9 @@ var greeterCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var greeterInstallCmd = &cobra.Command{
|
||||
Use: "install",
|
||||
Short: "Install and configure DMS greeter",
|
||||
Long: "Install greetd and configure it to use DMS as the greeter interface",
|
||||
PreRunE: requireMutableSystemCommand,
|
||||
Use: "install",
|
||||
Short: "Install and configure DMS greeter",
|
||||
Long: "Install greetd and configure it to use DMS as the greeter interface",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := installGreeter(); err != nil {
|
||||
log.Fatalf("Error installing greeter: %v", err)
|
||||
@@ -36,39 +34,20 @@ var greeterInstallCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var greeterSyncCmd = &cobra.Command{
|
||||
Use: "sync",
|
||||
Short: "Sync DMS theme and settings with greeter",
|
||||
Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen",
|
||||
PreRunE: requireMutableSystemCommand,
|
||||
Use: "sync",
|
||||
Short: "Sync DMS theme and settings with greeter",
|
||||
Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
yes, _ := cmd.Flags().GetBool("yes")
|
||||
auth, _ := cmd.Flags().GetBool("auth")
|
||||
local, _ := cmd.Flags().GetBool("local")
|
||||
term, _ := cmd.Flags().GetBool("terminal")
|
||||
if term {
|
||||
if err := syncInTerminal(yes, auth, local); err != nil {
|
||||
log.Fatalf("Error launching sync in terminal: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err := syncGreeter(yes, auth, local); err != nil {
|
||||
if err := syncGreeter(); err != nil {
|
||||
log.Fatalf("Error syncing greeter: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
greeterSyncCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode: skip prompts, use defaults (for UI)")
|
||||
greeterSyncCmd.Flags().BoolP("terminal", "t", false, "Run sync in a new terminal (for entering sudo password); terminal auto-closes when done")
|
||||
greeterSyncCmd.Flags().BoolP("auth", "a", false, "Configure PAM for fingerprint and U2F (adds both if modules exist); overrides UI toggles")
|
||||
greeterSyncCmd.Flags().BoolP("local", "l", false, "Developer mode: force greetd config to use a local DMS checkout path")
|
||||
}
|
||||
|
||||
var greeterEnableCmd = &cobra.Command{
|
||||
Use: "enable",
|
||||
Short: "Enable DMS greeter in greetd config",
|
||||
Long: "Configure greetd to use DMS as the greeter",
|
||||
PreRunE: requireMutableSystemCommand,
|
||||
Use: "enable",
|
||||
Short: "Enable DMS greeter in greetd config",
|
||||
Long: "Configure greetd to use DMS as the greeter",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := enableGreeter(); err != nil {
|
||||
log.Fatalf("Error enabling greeter: %v", err)
|
||||
@@ -98,28 +77,6 @@ func installGreeter() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Debian/openSUSE
|
||||
greeter.TryInstallGreeterPackage(logFunc, "")
|
||||
if isPackageOnlyGreeterDistro() && !greeter.IsGreeterPackaged() {
|
||||
return fmt.Errorf("dms-greeter must be installed from distro packages on this distribution. %s", packageInstallHint())
|
||||
}
|
||||
if greeter.IsGreeterPackaged() && greeter.HasLegacyLocalGreeterWrapper() {
|
||||
return fmt.Errorf("legacy manual wrapper detected at /usr/local/bin/dms-greeter; remove it before using packaged dms-greeter: sudo rm -f /usr/local/bin/dms-greeter")
|
||||
}
|
||||
|
||||
// If already fully configured, prompt the user
|
||||
if isGreeterEnabled() {
|
||||
fmt.Print("\nGreeter is already installed and configured. Re-run to re-sync settings and permissions? [Y/n]: ")
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
response = strings.TrimSpace(strings.ToLower(response))
|
||||
if response == "n" || response == "no" {
|
||||
fmt.Println("Run 'dms greeter sync' to re-sync theme and settings at any time.")
|
||||
return nil
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
fmt.Println("\nDetecting DMS installation...")
|
||||
dmsPath, err := greeter.DetectDMSPath()
|
||||
if err != nil {
|
||||
@@ -157,122 +114,40 @@ func installGreeter() error {
|
||||
}
|
||||
|
||||
fmt.Println("\nConfiguring greetd...")
|
||||
// Use empty path when packaged (greeter finds /usr/share/quickshell/dms-greeter); else use user's DMS path
|
||||
greeterPathForConfig := ""
|
||||
if !greeter.IsGreeterPackaged() {
|
||||
greeterPathForConfig = dmsPath
|
||||
}
|
||||
if err := greeter.ConfigureGreetd(greeterPathForConfig, selectedCompositor, logFunc, ""); err != nil {
|
||||
if err := greeter.ConfigureGreetd(dmsPath, selectedCompositor, logFunc, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("\nSynchronizing DMS configurations...")
|
||||
if err := greeter.SyncDMSConfigs(dmsPath, selectedCompositor, logFunc, "", false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ensureGraphicalTarget(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := handleConflictingDisplayManagers(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ensureGreetdEnabled(); err != nil {
|
||||
if err := greeter.SyncDMSConfigs(dmsPath, selectedCompositor, logFunc, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("\n=== Installation Complete ===")
|
||||
fmt.Println("\nTo start the greeter now, run:")
|
||||
fmt.Println("\nTo test the greeter, run:")
|
||||
fmt.Println(" sudo systemctl start greetd")
|
||||
fmt.Println("\nOr reboot to see the greeter at next boot.")
|
||||
fmt.Println("\nTo enable on boot, run:")
|
||||
fmt.Println(" sudo systemctl enable --now greetd")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func syncInTerminal(nonInteractive bool, forceAuth bool, local bool) error {
|
||||
syncFlags := make([]string, 0, 3)
|
||||
if nonInteractive {
|
||||
syncFlags = append(syncFlags, "--yes")
|
||||
}
|
||||
if forceAuth {
|
||||
syncFlags = append(syncFlags, "--auth")
|
||||
}
|
||||
if local {
|
||||
syncFlags = append(syncFlags, "--local")
|
||||
}
|
||||
shellSyncCmd := "dms greeter sync"
|
||||
if len(syncFlags) > 0 {
|
||||
shellSyncCmd += " " + strings.Join(syncFlags, " ")
|
||||
}
|
||||
shellCmd := shellSyncCmd + `; echo; echo "Sync finished. Closing in 3 seconds..."; sleep 3`
|
||||
terminals := []struct {
|
||||
name string
|
||||
args []string
|
||||
}{
|
||||
{"gnome-terminal", []string{"--", "bash", "-c", shellCmd}},
|
||||
{"konsole", []string{"-e", "bash", "-c", shellCmd}},
|
||||
{"xfce4-terminal", []string{"-e", "bash -c \"" + strings.ReplaceAll(shellCmd, `"`, `\"`) + "\""}},
|
||||
{"ghostty", []string{"-e", "bash", "-c", shellCmd}},
|
||||
{"wezterm", []string{"start", "--", "bash", "-c", shellCmd}},
|
||||
{"alacritty", []string{"-e", "bash", "-c", shellCmd}},
|
||||
{"kitty", []string{"bash", "-c", shellCmd}},
|
||||
{"xterm", []string{"-e", "bash -c \"" + strings.ReplaceAll(shellCmd, `"`, `\"`) + "\""}},
|
||||
}
|
||||
for _, t := range terminals {
|
||||
if _, err := exec.LookPath(t.name); err != nil {
|
||||
continue
|
||||
}
|
||||
cmd := exec.Command(t.name, t.args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Start(); err != nil {
|
||||
continue
|
||||
}
|
||||
_ = cmd.Process.Release()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("no terminal emulator found (tried: gnome-terminal, konsole, xfce4-terminal, ghostty, wezterm, alacritty, kitty, xterm)")
|
||||
}
|
||||
|
||||
func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
||||
if !nonInteractive {
|
||||
fmt.Println("=== DMS Greeter Theme Sync ===")
|
||||
fmt.Println()
|
||||
}
|
||||
func syncGreeter() error {
|
||||
fmt.Println("=== DMS Greeter Theme Sync ===")
|
||||
fmt.Println()
|
||||
|
||||
logFunc := func(msg string) {
|
||||
fmt.Println(msg)
|
||||
}
|
||||
|
||||
if !nonInteractive {
|
||||
fmt.Println("Detecting DMS installation...")
|
||||
}
|
||||
var dmsPath string
|
||||
var err error
|
||||
if local {
|
||||
dmsPath, err = resolveLocalDMSPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !nonInteractive {
|
||||
fmt.Printf("✓ Using local DMS path: %s\n", dmsPath)
|
||||
}
|
||||
} else {
|
||||
dmsPath, err = greeter.DetectDMSPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !nonInteractive {
|
||||
fmt.Printf("✓ Found DMS at: %s\n", dmsPath)
|
||||
}
|
||||
fmt.Println("Detecting DMS installation...")
|
||||
dmsPath, err := greeter.DetectDMSPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("✓ Found DMS at: %s\n", dmsPath)
|
||||
|
||||
if !isGreeterEnabled() {
|
||||
if nonInteractive {
|
||||
return fmt.Errorf("greeter is not enabled; run 'dms greeter install' or 'dms greeter enable' first")
|
||||
}
|
||||
fmt.Println("\n⚠ DMS greeter is not enabled in greetd config.")
|
||||
fmt.Print("Would you like to enable it now? (Y/n): ")
|
||||
|
||||
@@ -289,12 +164,9 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
cacheDir := greeter.GreeterCacheDir
|
||||
cacheDir := "/var/cache/dms-greeter"
|
||||
if _, err := os.Stat(cacheDir); os.IsNotExist(err) {
|
||||
logFunc("Cache directory not found — attempting to create it...")
|
||||
if createErr := greeter.EnsureGreeterCacheDir(logFunc, ""); createErr != nil {
|
||||
return fmt.Errorf("greeter cache directory not found at %s and could not be created: %w\nRun: sudo mkdir -p %s && sudo chown greeter:greeter %s", cacheDir, createErr, cacheDir, cacheDir)
|
||||
}
|
||||
return fmt.Errorf("greeter cache directory not found at %s\nPlease install the greeter first", cacheDir)
|
||||
}
|
||||
|
||||
greeterGroup := greeter.DetectGreeterGroup()
|
||||
@@ -313,9 +185,6 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
||||
|
||||
inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup)
|
||||
if !inGreeterGroup {
|
||||
if nonInteractive {
|
||||
return fmt.Errorf("user must be in the %s group; run 'dms greeter sync' from a terminal to add", greeterGroup)
|
||||
}
|
||||
fmt.Printf("\n⚠ Warning: You are not in the %s group.\n", greeterGroup)
|
||||
fmt.Printf("Would you like to add your user to the %s group? (Y/n): ", greeterGroup)
|
||||
|
||||
@@ -347,14 +216,8 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
||||
return fmt.Errorf("no supported compositors found")
|
||||
case 1:
|
||||
compositor = compositors[0]
|
||||
if !nonInteractive {
|
||||
fmt.Printf("✓ Using compositor: %s\n", compositor)
|
||||
}
|
||||
fmt.Printf("✓ Using compositor: %s\n", compositor)
|
||||
default:
|
||||
if nonInteractive {
|
||||
compositor = compositors[0]
|
||||
break
|
||||
}
|
||||
var err error
|
||||
compositor, err = promptCompositorChoice(compositors)
|
||||
if err != nil {
|
||||
@@ -362,49 +225,8 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
||||
}
|
||||
fmt.Printf("✓ Selected compositor: %s\n", compositor)
|
||||
}
|
||||
} else if !nonInteractive {
|
||||
fmt.Printf("✓ Detected compositor from config: %s\n", compositor)
|
||||
}
|
||||
|
||||
if local {
|
||||
localWrapperScript := filepath.Join(dmsPath, "Modules", "Greetd", "assets", "dms-greeter")
|
||||
restoreWrapperOverride := func() {}
|
||||
if info, statErr := os.Stat(localWrapperScript); statErr == nil && !info.IsDir() {
|
||||
previousWrapperOverride, hadWrapperOverride := os.LookupEnv("DMS_GREETER_WRAPPER_CMD")
|
||||
wrapperCmdOverride := "/usr/bin/bash " + localWrapperScript
|
||||
_ = os.Setenv("DMS_GREETER_WRAPPER_CMD", wrapperCmdOverride)
|
||||
restoreWrapperOverride = func() {
|
||||
if hadWrapperOverride {
|
||||
_ = os.Setenv("DMS_GREETER_WRAPPER_CMD", previousWrapperOverride)
|
||||
} else {
|
||||
_ = os.Unsetenv("DMS_GREETER_WRAPPER_CMD")
|
||||
}
|
||||
}
|
||||
if !nonInteractive {
|
||||
fmt.Printf("✓ Using local greeter wrapper script: %s\n", localWrapperScript)
|
||||
}
|
||||
} else if !nonInteractive {
|
||||
fmt.Printf("ℹ Local wrapper script not found at %s; using system wrapper.\n", localWrapperScript)
|
||||
}
|
||||
|
||||
fmt.Println("\nUpdating greetd command to use local DMS path...")
|
||||
err := greeter.ConfigureGreetd(dmsPath, compositor, logFunc, "")
|
||||
restoreWrapperOverride()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to apply local greeter path: %w", err)
|
||||
}
|
||||
if !nonInteractive {
|
||||
fmt.Println("ℹ Local mode applies both DMS path override (-p) and local wrapper behavior when available.")
|
||||
}
|
||||
} else {
|
||||
greeterPathForConfig := ""
|
||||
if !greeter.IsGreeterPackaged() {
|
||||
greeterPathForConfig = dmsPath
|
||||
}
|
||||
fmt.Println("\nUpdating greetd command...")
|
||||
if err := greeter.ConfigureGreetd(greeterPathForConfig, compositor, logFunc, ""); err != nil {
|
||||
return fmt.Errorf("failed to update greetd command: %w", err)
|
||||
}
|
||||
fmt.Printf("✓ Detected compositor from config: %s\n", compositor)
|
||||
}
|
||||
|
||||
fmt.Println("\nSetting up permissions and ACLs...")
|
||||
@@ -413,108 +235,17 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
||||
}
|
||||
|
||||
fmt.Println("\nSynchronizing DMS configurations...")
|
||||
if err := greeter.SyncDMSConfigs(dmsPath, compositor, logFunc, "", forceAuth); err != nil {
|
||||
if err := greeter.SyncDMSConfigs(dmsPath, compositor, logFunc, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("\n=== Sync Complete ===")
|
||||
fmt.Println("\nYour theme, settings, and wallpaper configuration have been synced with the greeter.")
|
||||
if forceAuth {
|
||||
fmt.Println("PAM has been configured for fingerprint and U2F (where modules exist).")
|
||||
}
|
||||
fmt.Println("The changes will be visible on the next login screen.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasDmsShellQml(dir string) bool {
|
||||
info, err := os.Stat(filepath.Join(dir, "shell.qml"))
|
||||
return err == nil && !info.IsDir()
|
||||
}
|
||||
|
||||
func resolveDMSLocalCandidate(path string) (string, bool) {
|
||||
if path == "" {
|
||||
return "", false
|
||||
}
|
||||
if hasDmsShellQml(path) {
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return path, true
|
||||
}
|
||||
return abs, true
|
||||
}
|
||||
|
||||
quickshellPath := filepath.Join(path, "quickshell")
|
||||
if hasDmsShellQml(quickshellPath) {
|
||||
abs, err := filepath.Abs(quickshellPath)
|
||||
if err != nil {
|
||||
return quickshellPath, true
|
||||
}
|
||||
return abs, true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
func resolveLocalDMSPath() (string, error) {
|
||||
if override := strings.TrimSpace(os.Getenv("DMS_LOCAL_PATH")); override != "" {
|
||||
if resolved, ok := resolveDMSLocalCandidate(override); ok {
|
||||
return resolved, nil
|
||||
}
|
||||
return "", fmt.Errorf("DMS_LOCAL_PATH is set but does not point to a valid DMS quickshell path: %s", override)
|
||||
}
|
||||
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get current directory: %w", err)
|
||||
}
|
||||
|
||||
dir := wd
|
||||
for {
|
||||
if resolved, ok := resolveDMSLocalCandidate(dir); ok {
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
break
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err == nil && homeDir != "" {
|
||||
for _, candidate := range []string{
|
||||
filepath.Join(homeDir, "dms"),
|
||||
filepath.Join(homeDir, "DankMaterialShell"),
|
||||
filepath.Join(homeDir, "dankmaterialshell"),
|
||||
filepath.Join(homeDir, "projects", "dms"),
|
||||
filepath.Join(homeDir, "src", "dms"),
|
||||
} {
|
||||
if resolved, ok := resolveDMSLocalCandidate(candidate); ok {
|
||||
return resolved, nil
|
||||
}
|
||||
}
|
||||
|
||||
if entries, readErr := os.ReadDir(homeDir); readErr == nil {
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := strings.ToLower(entry.Name())
|
||||
if !strings.Contains(name, "dms") && !strings.Contains(name, "dank") {
|
||||
continue
|
||||
}
|
||||
if resolved, ok := resolveDMSLocalCandidate(filepath.Join(homeDir, entry.Name())); ok {
|
||||
return resolved, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("could not locate a local DMS checkout from %s; run from repo root or set DMS_LOCAL_PATH=/absolute/path/to/repo", wd)
|
||||
}
|
||||
|
||||
func disableDisplayManager(dmName string) (bool, error) {
|
||||
state, err := getSystemdServiceState(dmName)
|
||||
if err != nil {
|
||||
@@ -596,29 +327,20 @@ func ensureGreetdEnabled() error {
|
||||
fmt.Println(" ✓ Unmasked greetd")
|
||||
}
|
||||
|
||||
if state.EnabledState == "enabled" || state.EnabledState == "enabled-runtime" {
|
||||
fmt.Println(" Reasserting greetd as active display manager...")
|
||||
} else {
|
||||
switch state.EnabledState {
|
||||
case "disabled", "masked", "masked-runtime":
|
||||
fmt.Println(" Enabling greetd service...")
|
||||
}
|
||||
|
||||
enableCmd := exec.Command("sudo", "systemctl", "enable", "--force", "greetd")
|
||||
enableCmd.Stdout = os.Stdout
|
||||
enableCmd.Stderr = os.Stderr
|
||||
if err := enableCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to enable greetd: %w", err)
|
||||
}
|
||||
|
||||
enabledState, _, verifyErr := checkSystemdServiceEnabled("greetd")
|
||||
if verifyErr != nil {
|
||||
fmt.Printf(" ⚠ Warning: Could not verify greetd enabled state: %v\n", verifyErr)
|
||||
} else {
|
||||
switch enabledState {
|
||||
case "enabled", "enabled-runtime", "static", "indirect", "alias":
|
||||
fmt.Printf(" ✓ greetd enabled (state: %s)\n", enabledState)
|
||||
default:
|
||||
return fmt.Errorf("greetd is still in state '%s' after enable operation", enabledState)
|
||||
enableCmd := exec.Command("sudo", "systemctl", "enable", "greetd")
|
||||
enableCmd.Stdout = os.Stdout
|
||||
enableCmd.Stderr = os.Stderr
|
||||
if err := enableCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to enable greetd: %w", err)
|
||||
}
|
||||
fmt.Println(" ✓ Enabled greetd service")
|
||||
case "enabled", "enabled-runtime":
|
||||
fmt.Println(" ✓ greetd is already enabled")
|
||||
default:
|
||||
fmt.Printf(" ℹ greetd is in state '%s' (should work, no action needed)\n", state.EnabledState)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -716,30 +438,18 @@ func enableGreeter() error {
|
||||
configPath := "/etc/greetd/config.toml"
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("greetd config not found at %s\nPlease install greetd first", configPath)
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to access greetd config at %s: %w", configPath, err)
|
||||
}
|
||||
|
||||
if greeter.IsGreeterPackaged() && greeter.HasLegacyLocalGreeterWrapper() {
|
||||
return fmt.Errorf("legacy manual wrapper detected at /usr/local/bin/dms-greeter; remove it before using packaged dms-greeter: sudo rm -f /usr/local/bin/dms-greeter")
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read greetd config: %w", err)
|
||||
}
|
||||
|
||||
configAlreadyCorrect := isGreeterEnabled()
|
||||
configuredCompositor := detectConfiguredCompositor()
|
||||
|
||||
logFunc := func(msg string) {
|
||||
fmt.Println(msg)
|
||||
}
|
||||
configContent := string(data)
|
||||
configAlreadyCorrect := strings.Contains(configContent, "dms-greeter")
|
||||
|
||||
if configAlreadyCorrect {
|
||||
fmt.Println("✓ Greeter is already configured with dms-greeter")
|
||||
if configuredCompositor != "" {
|
||||
fmt.Printf("✓ Configured compositor: %s\n", configuredCompositor)
|
||||
}
|
||||
|
||||
if err := greeter.EnsureGreeterCacheDir(logFunc, ""); err != nil {
|
||||
fmt.Printf("⚠ Could not create cache directory: %v\n Run: sudo mkdir -p %s && sudo chown greeter:greeter %s\n", err, greeter.GreeterCacheDir, greeter.GreeterCacheDir)
|
||||
}
|
||||
|
||||
if err := ensureGraphicalTarget(); err != nil {
|
||||
return err
|
||||
@@ -786,22 +496,68 @@ func enableGreeter() error {
|
||||
fmt.Printf("✓ Selected compositor: %s\n", selectedCompositor)
|
||||
}
|
||||
|
||||
greeterPathForConfig := ""
|
||||
if !greeter.IsGreeterPackaged() {
|
||||
dmsPath, err := greeter.DetectDMSPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to detect DMS path for manual greeter configuration: %w", err)
|
||||
}
|
||||
greeterPathForConfig = dmsPath
|
||||
backupPath := configPath + ".backup"
|
||||
backupCmd := exec.Command("sudo", "cp", configPath, backupPath)
|
||||
if err := backupCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to backup config: %w", err)
|
||||
}
|
||||
if err := greeter.ConfigureGreetd(greeterPathForConfig, selectedCompositor, logFunc, ""); err != nil {
|
||||
return fmt.Errorf("failed to configure greetd: %w", err)
|
||||
fmt.Printf("✓ Backed up config to %s\n", backupPath)
|
||||
|
||||
lines := strings.Split(configContent, "\n")
|
||||
var newLines []string
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") {
|
||||
newLines = append(newLines, line)
|
||||
}
|
||||
}
|
||||
|
||||
if err := greeter.EnsureGreeterCacheDir(logFunc, ""); err != nil {
|
||||
fmt.Printf("⚠ Could not create cache directory: %v\n Run: sudo mkdir -p %s && sudo chown greeter:greeter %s\n", err, greeter.GreeterCacheDir, greeter.GreeterCacheDir)
|
||||
wrapperCmd, err := findCommandPath("dms-greeter")
|
||||
if err != nil {
|
||||
return fmt.Errorf("dms-greeter not found in PATH. Please ensure it is installed and accessible")
|
||||
}
|
||||
|
||||
compositorLower := strings.ToLower(selectedCompositor)
|
||||
commandLine := fmt.Sprintf(`command = "%s --command %s"`, wrapperCmd, compositorLower)
|
||||
|
||||
var finalLines []string
|
||||
inDefaultSession := false
|
||||
commandAdded := false
|
||||
|
||||
for _, line := range newLines {
|
||||
finalLines = append(finalLines, line)
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
if trimmed == "[default_session]" {
|
||||
inDefaultSession = true
|
||||
}
|
||||
|
||||
if inDefaultSession && !commandAdded {
|
||||
if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") {
|
||||
finalLines = append(finalLines, commandLine)
|
||||
commandAdded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !commandAdded {
|
||||
finalLines = append(finalLines, commandLine)
|
||||
}
|
||||
|
||||
newConfig := strings.Join(finalLines, "\n")
|
||||
|
||||
tmpFile := "/tmp/greetd-config.toml"
|
||||
if err := os.WriteFile(tmpFile, []byte(newConfig), 0o644); err != nil {
|
||||
return fmt.Errorf("failed to write temp config: %w", err)
|
||||
}
|
||||
|
||||
moveCmd := exec.Command("sudo", "mv", tmpFile, configPath)
|
||||
if err := moveCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to update config: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Updated greetd configuration to use %s\n", selectedCompositor)
|
||||
|
||||
if err := ensureGraphicalTarget(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -823,204 +579,36 @@ func enableGreeter() error {
|
||||
}
|
||||
|
||||
func isGreeterEnabled() bool {
|
||||
command := readDefaultSessionCommand("/etc/greetd/config.toml")
|
||||
return command != "" && strings.Contains(command, "dms-greeter")
|
||||
data, err := os.ReadFile("/etc/greetd/config.toml")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(string(data), "dms-greeter")
|
||||
}
|
||||
|
||||
func detectConfiguredCompositor() string {
|
||||
command := strings.ToLower(readDefaultSessionCommand("/etc/greetd/config.toml"))
|
||||
switch {
|
||||
case strings.Contains(command, "--command niri"):
|
||||
return "niri"
|
||||
case strings.Contains(command, "--command hyprland"):
|
||||
return "hyprland"
|
||||
case strings.Contains(command, "--command sway"):
|
||||
return "sway"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func stripTomlComment(line string) string {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if idx := strings.Index(trimmed, "#"); idx >= 0 {
|
||||
return strings.TrimSpace(trimmed[:idx])
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
func parseTomlSection(line string) (string, bool) {
|
||||
trimmed := stripTomlComment(line)
|
||||
if len(trimmed) < 3 || !strings.HasPrefix(trimmed, "[") || !strings.HasSuffix(trimmed, "]") {
|
||||
return "", false
|
||||
}
|
||||
return strings.TrimSpace(trimmed[1 : len(trimmed)-1]), true
|
||||
}
|
||||
|
||||
func readDefaultSessionCommand(configPath string) string {
|
||||
data, err := os.ReadFile(configPath)
|
||||
data, err := os.ReadFile("/etc/greetd/config.toml")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
inDefaultSession := false
|
||||
for line := range strings.SplitSeq(string(data), "\n") {
|
||||
if section, ok := parseTomlSection(line); ok {
|
||||
inDefaultSession = section == "default_session"
|
||||
continue
|
||||
}
|
||||
|
||||
if !inDefaultSession {
|
||||
continue
|
||||
}
|
||||
|
||||
trimmed := stripTomlComment(line)
|
||||
if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(trimmed, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
command := strings.Trim(strings.TrimSpace(parts[1]), `"`)
|
||||
if command != "" {
|
||||
return command
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractGreeterCacheDirFromCommand(command string) string {
|
||||
if command == "" {
|
||||
return greeter.GreeterCacheDir
|
||||
}
|
||||
tokens := strings.Fields(command)
|
||||
for i := 0; i < len(tokens); i++ {
|
||||
token := strings.Trim(tokens[i], "\"")
|
||||
if token == "--cache-dir" && i+1 < len(tokens) {
|
||||
return strings.Trim(tokens[i+1], "\"")
|
||||
}
|
||||
if strings.HasPrefix(token, "--cache-dir=") {
|
||||
value := strings.TrimPrefix(token, "--cache-dir=")
|
||||
value = strings.Trim(value, "\"")
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
return greeter.GreeterCacheDir
|
||||
}
|
||||
|
||||
func extractGreeterWrapperFromCommand(command string) string {
|
||||
if command == "" {
|
||||
return ""
|
||||
}
|
||||
tokens := strings.Fields(command)
|
||||
if len(tokens) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.Trim(tokens[0], "\"")
|
||||
}
|
||||
|
||||
func extractGreeterPathOverrideFromCommand(command string) string {
|
||||
if command == "" {
|
||||
return ""
|
||||
}
|
||||
tokens := strings.Fields(command)
|
||||
for i := 0; i < len(tokens); i++ {
|
||||
token := strings.Trim(tokens[i], "\"")
|
||||
if (token == "-p" || token == "--path") && i+1 < len(tokens) {
|
||||
return strings.Trim(tokens[i+1], "\"")
|
||||
}
|
||||
if strings.HasPrefix(token, "--path=") {
|
||||
value := strings.TrimPrefix(token, "--path=")
|
||||
value = strings.Trim(value, "\"")
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func parseManagedGreeterPamAuth(pamText string) (managed bool, fingerprint bool, u2f bool, legacy bool) {
|
||||
if pamText == "" {
|
||||
return false, false, false, false
|
||||
}
|
||||
|
||||
lines := strings.Split(pamText, "\n")
|
||||
inManaged := false
|
||||
for _, line := range lines {
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
switch trimmed {
|
||||
case greeter.GreeterPamManagedBlockStart:
|
||||
managed = true
|
||||
inManaged = true
|
||||
continue
|
||||
case greeter.GreeterPamManagedBlockEnd:
|
||||
inManaged = false
|
||||
if !strings.HasPrefix(trimmed, "command") || !strings.Contains(trimmed, "dms-greeter") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(trimmed, "# DMS greeter fingerprint") || strings.HasPrefix(trimmed, "# DMS greeter U2F") {
|
||||
legacy = true
|
||||
}
|
||||
if !inManaged {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(trimmed, "pam_fprintd") {
|
||||
fingerprint = true
|
||||
}
|
||||
if strings.Contains(trimmed, "pam_u2f") {
|
||||
u2f = true
|
||||
switch {
|
||||
case strings.Contains(trimmed, "--command niri"):
|
||||
return "niri"
|
||||
case strings.Contains(trimmed, "--command hyprland"):
|
||||
return "hyprland"
|
||||
case strings.Contains(trimmed, "--command sway"):
|
||||
return "sway"
|
||||
}
|
||||
}
|
||||
|
||||
return managed, fingerprint, u2f, legacy
|
||||
}
|
||||
|
||||
func packageInstallHint() string {
|
||||
osInfo, err := distros.GetOSInfo()
|
||||
if err != nil {
|
||||
return "Install package: dms-greeter"
|
||||
}
|
||||
config, exists := distros.Registry[osInfo.Distribution.ID]
|
||||
if !exists {
|
||||
return "Install package: dms-greeter"
|
||||
}
|
||||
|
||||
switch config.Family {
|
||||
case distros.FamilyDebian:
|
||||
return "Install with 'sudo apt install dms-greeter' (requires DankLinux OBS repo — see https://danklinux.com/docs/dankgreeter/installation#debian)"
|
||||
case distros.FamilySUSE:
|
||||
return "Install with 'sudo zypper install dms-greeter' (requires DankLinux OBS repo — see https://danklinux.com/docs/dankgreeter/installation#opensuse)"
|
||||
case distros.FamilyUbuntu:
|
||||
return "Install with 'sudo apt install dms-greeter' (requires ppa:avengemedia/danklinux: sudo add-apt-repository ppa:avengemedia/danklinux)"
|
||||
case distros.FamilyFedora:
|
||||
return "Install with 'sudo dnf install dms-greeter' (requires COPR: sudo dnf copr enable avengemedia/danklinux)"
|
||||
case distros.FamilyArch:
|
||||
return "Install from AUR with 'paru -S greetd-dms-greeter-git' or 'yay -S greetd-dms-greeter-git'"
|
||||
default:
|
||||
return "Run 'dms greeter install' to install greeter"
|
||||
}
|
||||
}
|
||||
|
||||
func isPackageOnlyGreeterDistro() bool {
|
||||
osInfo, err := distros.GetOSInfo()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
config, exists := distros.Registry[osInfo.Distribution.ID]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
return config.Family == distros.FamilyDebian ||
|
||||
config.Family == distros.FamilySUSE ||
|
||||
config.Family == distros.FamilyUbuntu ||
|
||||
config.Family == distros.FamilyFedora ||
|
||||
config.Family == distros.FamilyArch
|
||||
return ""
|
||||
}
|
||||
|
||||
func promptCompositorChoice(compositors []string) (string, error) {
|
||||
@@ -1059,40 +647,39 @@ func checkGreeterStatus() error {
|
||||
}
|
||||
|
||||
configPath := "/etc/greetd/config.toml"
|
||||
configuredCommand := ""
|
||||
allGood := true
|
||||
fmt.Println("Greeter Configuration:")
|
||||
if _, err := os.ReadFile(configPath); err == nil {
|
||||
configuredCommand = readDefaultSessionCommand(configPath)
|
||||
if configuredCommand != "" && strings.Contains(configuredCommand, "dms-greeter") {
|
||||
fmt.Println(" ✓ Greeter is enabled")
|
||||
if wrapper := extractGreeterWrapperFromCommand(configuredCommand); wrapper != "" {
|
||||
fmt.Printf(" Wrapper: %s\n", wrapper)
|
||||
}
|
||||
if pathOverride := extractGreeterPathOverrideFromCommand(configuredCommand); pathOverride != "" {
|
||||
fmt.Printf(" DMS path override: %s\n", pathOverride)
|
||||
}
|
||||
if data, err := os.ReadFile(configPath); err == nil {
|
||||
configContent := string(data)
|
||||
if strings.Contains(configContent, "dms-greeter") {
|
||||
lines := strings.SplitSeq(configContent, "\n")
|
||||
for line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "command =") || strings.HasPrefix(trimmed, "command=") {
|
||||
parts := strings.SplitN(trimmed, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
command := strings.Trim(strings.TrimSpace(parts[1]), `"`)
|
||||
fmt.Println(" ✓ Greeter is enabled")
|
||||
|
||||
compositor := detectConfiguredCompositor()
|
||||
switch compositor {
|
||||
case "niri":
|
||||
fmt.Println(" Compositor: niri")
|
||||
case "hyprland":
|
||||
fmt.Println(" Compositor: Hyprland")
|
||||
case "sway":
|
||||
fmt.Println(" Compositor: sway")
|
||||
default:
|
||||
fmt.Println(" Compositor: unknown")
|
||||
if strings.Contains(command, "--command niri") {
|
||||
fmt.Println(" Compositor: niri")
|
||||
} else if strings.Contains(command, "--command hyprland") {
|
||||
fmt.Println(" Compositor: Hyprland")
|
||||
} else if strings.Contains(command, "--command sway") {
|
||||
fmt.Println(" Compositor: sway")
|
||||
} else {
|
||||
fmt.Println(" Compositor: unknown")
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Println(" ✗ Greeter is NOT enabled")
|
||||
fmt.Println(" Run 'dms greeter enable' to enable it")
|
||||
allGood = false
|
||||
}
|
||||
} else {
|
||||
fmt.Println(" ✗ Greeter config not found")
|
||||
fmt.Printf(" %s\n", packageInstallHint())
|
||||
allGood = false
|
||||
fmt.Println(" Run 'dms greeter install' to install greeter")
|
||||
}
|
||||
|
||||
fmt.Println("\nGroup Membership:")
|
||||
@@ -1102,26 +689,21 @@ func checkGreeterStatus() error {
|
||||
return fmt.Errorf("failed to check groups: %w", err)
|
||||
}
|
||||
|
||||
greeterGroup := greeter.DetectGreeterGroup()
|
||||
inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup)
|
||||
inGreeterGroup := strings.Contains(string(groupsOutput), "greeter")
|
||||
if inGreeterGroup {
|
||||
fmt.Printf(" ✓ User is in %s group\n", greeterGroup)
|
||||
fmt.Println(" ✓ User is in greeter group")
|
||||
} else {
|
||||
fmt.Printf(" ✗ User is NOT in %s group\n", greeterGroup)
|
||||
fmt.Println(" Run 'dms greeter sync' to set up group membership and permissions")
|
||||
fmt.Println(" ✗ User is NOT in greeter group")
|
||||
fmt.Println(" Run 'dms greeter install' to add user to greeter group")
|
||||
}
|
||||
|
||||
cacheDir := extractGreeterCacheDirFromCommand(configuredCommand)
|
||||
cacheDir := "/var/cache/dms-greeter"
|
||||
fmt.Println("\nGreeter Cache Directory:")
|
||||
fmt.Printf(" Effective cache dir: %s\n", cacheDir)
|
||||
if cacheDir != greeter.GreeterCacheDir {
|
||||
fmt.Printf(" ⚠ Non-default cache dir detected (default: %s)\n", greeter.GreeterCacheDir)
|
||||
}
|
||||
if stat, err := os.Stat(cacheDir); err == nil && stat.IsDir() {
|
||||
fmt.Printf(" ✓ %s exists\n", cacheDir)
|
||||
} else {
|
||||
fmt.Printf(" ✗ %s not found\n", cacheDir)
|
||||
fmt.Printf(" %s\n", packageInstallHint())
|
||||
fmt.Println(" Run 'dms greeter install' to create cache directory")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1148,6 +730,7 @@ func checkGreeterStatus() error {
|
||||
},
|
||||
}
|
||||
|
||||
allGood := true
|
||||
for _, link := range symlinks {
|
||||
targetInfo, err := os.Lstat(link.target)
|
||||
if err != nil {
|
||||
@@ -1186,80 +769,11 @@ func checkGreeterStatus() error {
|
||||
fmt.Printf(" ✓ %s: synced correctly\n", link.desc)
|
||||
}
|
||||
|
||||
fmt.Println("\nGreeter Wallpaper Override:")
|
||||
overridePath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
|
||||
if stat, err := os.Stat(overridePath); err == nil && !stat.IsDir() {
|
||||
fmt.Printf(" ✓ Override file present: %s\n", overridePath)
|
||||
} else if os.IsNotExist(err) {
|
||||
fmt.Println(" ℹ Override file not present (desktop/session wallpaper fallback in effect)")
|
||||
} else if err != nil {
|
||||
fmt.Printf(" ✗ Could not inspect override file: %v\n", err)
|
||||
allGood = false
|
||||
} else {
|
||||
fmt.Printf(" ✗ Override path is not a regular file: %s\n", overridePath)
|
||||
allGood = false
|
||||
}
|
||||
|
||||
fmt.Println("\nGreeter PAM Authentication (DMS-managed block):")
|
||||
if greeter.IsNixOS() {
|
||||
fmt.Println(" ℹ NixOS detected: PAM is managed by NixOS modules.")
|
||||
fmt.Println(" Configure fingerprint/U2F via your greetd NixOS module (security.pam.services.greetd).")
|
||||
fmt.Println()
|
||||
if allGood && inGreeterGroup {
|
||||
fmt.Println("✓ All checks passed! Greeter is properly configured.")
|
||||
} else if !allGood {
|
||||
fmt.Println("⚠ Some issues detected. Run 'dms greeter sync' to repair configuration.")
|
||||
} else if !inGreeterGroup {
|
||||
fmt.Printf("⚠ User is not in %s group. Run 'dms greeter sync' after adding group membership.\n", greeterGroup)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
greetdPamPath := "/etc/pam.d/greetd"
|
||||
pamData, err := os.ReadFile(greetdPamPath)
|
||||
if err != nil {
|
||||
fmt.Printf(" ✗ Failed to read %s: %v\n", greetdPamPath, err)
|
||||
allGood = false
|
||||
} else {
|
||||
managed, managedFprint, managedU2f, legacyManaged := parseManagedGreeterPamAuth(string(pamData))
|
||||
if managed {
|
||||
fmt.Println(" ✓ Managed auth block present")
|
||||
if managedFprint {
|
||||
fmt.Println(" - fingerprint: enabled")
|
||||
} else {
|
||||
fmt.Println(" - fingerprint: disabled")
|
||||
}
|
||||
if managedU2f {
|
||||
fmt.Println(" - security key (U2F): enabled")
|
||||
} else {
|
||||
fmt.Println(" - security key (U2F): disabled")
|
||||
}
|
||||
} else {
|
||||
fmt.Println(" ℹ No managed auth block present (fingerprint/U2F disabled for greeter)")
|
||||
}
|
||||
if legacyManaged {
|
||||
fmt.Println(" ⚠ Legacy unmanaged DMS PAM lines detected. Run 'dms greeter sync' to normalize.")
|
||||
allGood = false
|
||||
}
|
||||
includedFprintFile := greeter.DetectIncludedPamModule(string(pamData), "pam_fprintd.so")
|
||||
if managedFprint {
|
||||
if includedFprintFile != "" {
|
||||
fmt.Printf(" ⚠ pam_fprintd found in both DMS managed block and %s.\n", includedFprintFile)
|
||||
fmt.Println(" Double fingerprint auth detected — run 'dms greeter sync' to resolve.")
|
||||
allGood = false
|
||||
}
|
||||
} else if includedFprintFile != "" {
|
||||
fmt.Printf(" ℹ Fingerprint auth is enabled via included %s.\n", includedFprintFile)
|
||||
fmt.Println(" The DMS toggle only controls the managed block; disable fingerprint in authselect/pam-auth-update for password-only greeter login.")
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
if allGood && inGreeterGroup {
|
||||
fmt.Println("✓ All checks passed! Greeter is properly configured.")
|
||||
} else if !allGood {
|
||||
fmt.Println("⚠ Some issues detected. Run 'dms greeter sync' to repair configuration.")
|
||||
} else if !inGreeterGroup {
|
||||
fmt.Printf("⚠ User is not in %s group. Run 'dms greeter sync' after adding group membership.\n", greeterGroup)
|
||||
fmt.Println("⚠ Some issues detected. Run 'dms greeter sync' to fix symlinks.")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -3,9 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"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) {
|
||||
opts := buildMatugenOptions(cmd)
|
||||
err := matugen.Run(opts)
|
||||
switch {
|
||||
case errors.Is(err, matugen.ErrNoChanges):
|
||||
os.Exit(2)
|
||||
case err != nil:
|
||||
if err := matugen.Run(opts); err != nil {
|
||||
log.Fatalf("Theme generation failed: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -135,11 +129,7 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
||||
if !wait {
|
||||
if err := sendServerRequestFireAndForget(request); err != nil {
|
||||
log.Info("Server unavailable, running synchronously")
|
||||
err := matugen.Run(opts)
|
||||
switch {
|
||||
case errors.Is(err, matugen.ErrNoChanges):
|
||||
os.Exit(2)
|
||||
case err != nil:
|
||||
if err := matugen.Run(opts); err != nil {
|
||||
log.Fatalf("Theme generation failed: %v", err)
|
||||
}
|
||||
return
|
||||
@@ -156,15 +146,11 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
||||
resp, ok := tryServerRequest(request)
|
||||
if !ok {
|
||||
log.Info("Server unavailable, running synchronously")
|
||||
err := matugen.Run(opts)
|
||||
switch {
|
||||
case errors.Is(err, matugen.ErrNoChanges):
|
||||
resultCh <- matugen.ErrNoChanges
|
||||
case err != nil:
|
||||
if err := matugen.Run(opts); err != nil {
|
||||
resultCh <- err
|
||||
default:
|
||||
resultCh <- nil
|
||||
return
|
||||
}
|
||||
resultCh <- nil
|
||||
return
|
||||
}
|
||||
if resp.Error != "" {
|
||||
@@ -176,10 +162,7 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
||||
|
||||
select {
|
||||
case err := <-resultCh:
|
||||
switch {
|
||||
case errors.Is(err, matugen.ErrNoChanges):
|
||||
os.Exit(2)
|
||||
case err != nil:
|
||||
if err != nil {
|
||||
log.Fatalf("Theme generation failed: %v", err)
|
||||
}
|
||||
fmt.Println("Theme generation completed")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,9 @@ import (
|
||||
)
|
||||
|
||||
var setupCmd = &cobra.Command{
|
||||
Use: "setup",
|
||||
Short: "Deploy DMS configurations",
|
||||
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
||||
PersistentPreRunE: requireMutableSystemCommand,
|
||||
Use: "setup",
|
||||
Short: "Deploy DMS configurations",
|
||||
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := runSetup(); err != nil {
|
||||
log.Fatalf("Error during setup: %v", err)
|
||||
|
||||
@@ -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", "greeter sync", "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)
|
||||
}
|
||||
@@ -18,7 +18,7 @@ func init() {
|
||||
runCmd.Flags().MarkHidden("daemon-child")
|
||||
|
||||
// Add subcommands to greeter
|
||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
||||
greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
||||
|
||||
// Add subcommands to setup
|
||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -7,6 +7,14 @@ import (
|
||||
"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 {
|
||||
cmd := exec.Command("pacman", "-Q", packageName)
|
||||
err := cmd.Run()
|
||||
|
||||
@@ -16,8 +16,6 @@ require (
|
||||
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.5
|
||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0
|
||||
github.com/yuin/goldmark v1.7.16
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
go.etcd.io/bbolt v1.4.3
|
||||
@@ -34,19 +32,15 @@ require (
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/fogleman/gg v1.3.0 // indirect
|
||||
github.com/go-git/gcfg/v2 v2.0.2 // indirect
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/kevinburke/ssh_config v1.6.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
)
|
||||
|
||||
12
core/go.sum
12
core/go.sum
@@ -58,8 +58,6 @@ github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
|
||||
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
@@ -77,8 +75,6 @@ github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
@@ -119,8 +115,6 @@ github.com/pilebones/go-udev v0.9.1 h1:uN72M1C1fgzhsVmBGEM8w9RD1JY4iVsPZpr+Z6rb3
|
||||
github.com/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo=
|
||||
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
||||
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
@@ -148,12 +142,6 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.5 h1:HCOe2bSjkhZyYoyyNaXNzh4DJZll6inVJQQw+8228Zk=
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.5/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw=
|
||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0 h1:chdyhEfRtUPgQtuPeaWVGQ/TQx4rE1PqeoW3U+53t34=
|
||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0/go.mod h1:O4MbzsotGCvy8upYPCR91j81dr5XLT7heuljcNXW+oQ=
|
||||
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
|
||||
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
|
||||
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package clipboard
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
||||
@@ -14,21 +12,17 @@ import (
|
||||
)
|
||||
|
||||
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 {
|
||||
return CopyReader(bytes.NewReader(data), mimeType, foreground, pasteOnce)
|
||||
}
|
||||
|
||||
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
|
||||
if !foreground {
|
||||
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"}
|
||||
if pasteOnce {
|
||||
args = append(args, "--paste-once")
|
||||
@@ -36,15 +30,11 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||
args = append(args, "--type", mimeType)
|
||||
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stdin = nil
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||
|
||||
if stdinSource, ok := data.(*os.File); ok {
|
||||
cmd.Stdin = stdinSource
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stdin pipe: %w", err)
|
||||
@@ -54,84 +44,16 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||
return fmt.Errorf("start: %w", err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(stdin, data); err != nil {
|
||||
if _, err := stdin.Write(data); err != nil {
|
||||
stdin.Close()
|
||||
return fmt.Errorf("write stdin: %w", err)
|
||||
}
|
||||
if err := stdin.Close(); err != nil {
|
||||
return fmt.Errorf("close stdin: %w", err)
|
||||
}
|
||||
stdin.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyServe(data []byte, mimeType string, pasteOnce bool) error {
|
||||
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)
|
||||
}
|
||||
|
||||
func copyServeReader(data io.Reader, 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("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("wayland connect: %w", err)
|
||||
@@ -217,18 +139,12 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
||||
|
||||
cancelled := make(chan struct{})
|
||||
pasted := make(chan struct{}, 1)
|
||||
sendErr := make(chan error, 1)
|
||||
|
||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||
defer syscall.Close(e.Fd)
|
||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||
defer file.Close()
|
||||
if err := writeTo(file); err != nil {
|
||||
select {
|
||||
case sendErr <- err:
|
||||
default:
|
||||
}
|
||||
}
|
||||
file.Write(data)
|
||||
select {
|
||||
case pasted <- struct{}{}:
|
||||
default:
|
||||
@@ -249,8 +165,6 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
||||
select {
|
||||
case <-cancelled:
|
||||
return nil
|
||||
case err := <-sendErr:
|
||||
return err
|
||||
case <-pasted:
|
||||
if pasteOnce {
|
||||
return nil
|
||||
|
||||
@@ -97,7 +97,6 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
|
||||
dependencies = append(dependencies, a.detectGit())
|
||||
dependencies = append(dependencies, a.detectWindowManager(wm))
|
||||
dependencies = append(dependencies, a.detectQuickshell())
|
||||
dependencies = append(dependencies, a.detectDMSGreeter())
|
||||
dependencies = append(dependencies, a.detectXDGPortal())
|
||||
dependencies = append(dependencies, a.detectAccountsService())
|
||||
|
||||
@@ -125,10 +124,6 @@ func (a *ArchDistribution) detectAccountsService() deps.Dependency {
|
||||
return a.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", a.packageInstalled("accountsservice"))
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) detectDMSGreeter() deps.Dependency {
|
||||
return a.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", a.packageInstalled("greetd-dms-greeter-git"))
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) packageInstalled(pkg string) bool {
|
||||
cmd := exec.Command("pacman", "-Q", pkg)
|
||||
err := cmd.Run()
|
||||
@@ -144,7 +139,6 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
|
||||
"dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]),
|
||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||
"quickshell": a.getQuickshellMapping(variants["quickshell"]),
|
||||
"dms-greeter": {Name: "greetd-dms-greeter-git", Repository: RepoTypeAUR},
|
||||
"matugen": a.getMatugenMapping(variants["matugen"]),
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeSystem},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
||||
|
||||
@@ -102,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 {
|
||||
return b.detectCommand("git", "Version control system")
|
||||
}
|
||||
|
||||
@@ -61,7 +61,6 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
||||
dependencies = append(dependencies, d.detectGit())
|
||||
dependencies = append(dependencies, d.detectWindowManager(wm))
|
||||
dependencies = append(dependencies, d.detectQuickshell())
|
||||
dependencies = append(dependencies, d.detectDMSGreeter())
|
||||
dependencies = append(dependencies, d.detectXDGPortal())
|
||||
dependencies = append(dependencies, d.detectAccountsService())
|
||||
|
||||
@@ -87,10 +86,6 @@ func (d *DebianDistribution) detectAccountsService() deps.Dependency {
|
||||
return d.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", d.packageInstalled("accountsservice"))
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) detectDMSGreeter() deps.Dependency {
|
||||
return d.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", d.packageInstalled("dms-greeter"))
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
||||
cmd := exec.Command("dpkg", "-l", pkg)
|
||||
err := cmd.Run()
|
||||
@@ -113,7 +108,6 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
||||
// DMS packages from OBS with variant support
|
||||
"dms (DankMaterialShell)": d.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||
"quickshell": d.getQuickshellMapping(variants["quickshell"]),
|
||||
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
|
||||
@@ -13,9 +13,6 @@ func init() {
|
||||
Register("fedora", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewFedoraDistribution(config, logChan)
|
||||
})
|
||||
Register("evernight", "#72B8DC", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewFedoraDistribution(config, logChan)
|
||||
})
|
||||
Register("nobara", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewFedoraDistribution(config, logChan)
|
||||
})
|
||||
@@ -78,7 +75,6 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
||||
dependencies = append(dependencies, f.detectGit())
|
||||
dependencies = append(dependencies, f.detectWindowManager(wm))
|
||||
dependencies = append(dependencies, f.detectQuickshell())
|
||||
dependencies = append(dependencies, f.detectDMSGreeter())
|
||||
dependencies = append(dependencies, f.detectXDGPortal())
|
||||
dependencies = append(dependencies, f.detectAccountsService())
|
||||
|
||||
@@ -124,7 +120,6 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
||||
|
||||
// COPR packages
|
||||
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
|
||||
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||
@@ -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 {
|
||||
return []string{
|
||||
"dnf-plugins-core",
|
||||
|
||||
@@ -55,7 +55,6 @@ const (
|
||||
PhaseAURPackages
|
||||
PhaseCursorTheme
|
||||
PhaseConfiguration
|
||||
PhaseGreeterSetup
|
||||
PhaseComplete
|
||||
)
|
||||
|
||||
|
||||
@@ -71,7 +71,6 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
|
||||
dependencies = append(dependencies, o.detectGit())
|
||||
dependencies = append(dependencies, o.detectWindowManager(wm))
|
||||
dependencies = append(dependencies, o.detectQuickshell())
|
||||
dependencies = append(dependencies, o.detectDMSGreeter())
|
||||
dependencies = append(dependencies, o.detectXDGPortal())
|
||||
dependencies = append(dependencies, o.detectAccountsService())
|
||||
|
||||
@@ -101,10 +100,6 @@ func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) detectDMSGreeter() deps.Dependency {
|
||||
return o.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", o.packageInstalled("dms-greeter"))
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||
return o.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||
}
|
||||
@@ -121,7 +116,6 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
||||
// DMS packages from OBS
|
||||
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||
"quickshell": o.getQuickshellMapping(variants["quickshell"]),
|
||||
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
|
||||
@@ -63,7 +63,6 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
||||
dependencies = append(dependencies, u.detectGit())
|
||||
dependencies = append(dependencies, u.detectWindowManager(wm))
|
||||
dependencies = append(dependencies, u.detectQuickshell())
|
||||
dependencies = append(dependencies, u.detectDMSGreeter())
|
||||
dependencies = append(dependencies, u.detectXDGPortal())
|
||||
dependencies = append(dependencies, u.detectAccountsService())
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) detectDMSGreeter() deps.Dependency {
|
||||
return u.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", u.packageInstalled("dms-greeter"))
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
|
||||
cmd := exec.Command("dpkg", "-l", pkg)
|
||||
err := cmd.Run()
|
||||
@@ -121,7 +116,6 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
||||
// DMS packages from PPAs
|
||||
"dms (DankMaterialShell)": u.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||
"quickshell": u.getQuickshellMapping(variants["quickshell"]),
|
||||
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package geolocation
|
||||
|
||||
import "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
|
||||
func NewClient() Client {
|
||||
geoclueClient, err := newGeoClueClient()
|
||||
if err != nil {
|
||||
log.Warnf("GeoClue2 unavailable: %v", err)
|
||||
return newSeededIpClient()
|
||||
}
|
||||
|
||||
loc, _ := geoclueClient.GetLocation()
|
||||
if loc.Latitude != 0 || loc.Longitude != 0 {
|
||||
log.Info("Using GeoClue2 location")
|
||||
return geoclueClient
|
||||
}
|
||||
|
||||
log.Info("GeoClue2 has no fix yet, seeding with IP location")
|
||||
ipLoc, err := fetchIPLocation()
|
||||
if err != nil {
|
||||
log.Warnf("IP location seed failed: %v", err)
|
||||
return geoclueClient
|
||||
}
|
||||
|
||||
log.Info("Seeded GeoClue2 with IP location")
|
||||
geoclueClient.SeedLocation(Location{Latitude: ipLoc.Latitude, Longitude: ipLoc.Longitude})
|
||||
return geoclueClient
|
||||
}
|
||||
|
||||
func newSeededIpClient() *IpClient {
|
||||
client := newIpClient()
|
||||
ipLoc, err := fetchIPLocation()
|
||||
if err != nil {
|
||||
log.Warnf("IP location also failed: %v", err)
|
||||
return client
|
||||
}
|
||||
|
||||
log.Info("Using IP location")
|
||||
client.currLocation.Latitude = ipLoc.Latitude
|
||||
client.currLocation.Longitude = ipLoc.Longitude
|
||||
return client
|
||||
}
|
||||
@@ -1,243 +0,0 @@
|
||||
package geolocation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
dbusGeoClueService = "org.freedesktop.GeoClue2"
|
||||
dbusGeoCluePath = "/org/freedesktop/GeoClue2"
|
||||
dbusGeoClueInterface = dbusGeoClueService
|
||||
|
||||
dbusGeoClueManagerPath = dbusGeoCluePath + "/Manager"
|
||||
dbusGeoClueManagerInterface = dbusGeoClueInterface + ".Manager"
|
||||
dbusGeoClueManagerGetClient = dbusGeoClueManagerInterface + ".GetClient"
|
||||
|
||||
dbusGeoClueClientInterface = dbusGeoClueInterface + ".Client"
|
||||
dbusGeoClueClientDesktopId = dbusGeoClueClientInterface + ".DesktopId"
|
||||
dbusGeoClueClientTimeThreshold = dbusGeoClueClientInterface + ".TimeThreshold"
|
||||
dbusGeoClueClientTimeStart = dbusGeoClueClientInterface + ".Start"
|
||||
dbusGeoClueClientTimeStop = dbusGeoClueClientInterface + ".Stop"
|
||||
dbusGeoClueClientLocationUpdated = dbusGeoClueClientInterface + ".LocationUpdated"
|
||||
|
||||
dbusGeoClueLocationInterface = dbusGeoClueInterface + ".Location"
|
||||
dbusGeoClueLocationLatitude = dbusGeoClueLocationInterface + ".Latitude"
|
||||
dbusGeoClueLocationLongitude = dbusGeoClueLocationInterface + ".Longitude"
|
||||
)
|
||||
|
||||
type GeoClueClient struct {
|
||||
currLocation *Location
|
||||
locationMutex sync.RWMutex
|
||||
|
||||
dbusConn *dbus.Conn
|
||||
clientPath dbus.ObjectPath
|
||||
signals chan *dbus.Signal
|
||||
|
||||
stopChan chan struct{}
|
||||
sigWG sync.WaitGroup
|
||||
|
||||
subscribers syncmap.Map[string, chan Location]
|
||||
}
|
||||
|
||||
func newGeoClueClient() (*GeoClueClient, error) {
|
||||
dbusConn, err := dbus.ConnectSystemBus()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("system bus connection failed: %w", err)
|
||||
}
|
||||
|
||||
c := &GeoClueClient{
|
||||
dbusConn: dbusConn,
|
||||
stopChan: make(chan struct{}),
|
||||
signals: make(chan *dbus.Signal, 256),
|
||||
|
||||
currLocation: &Location{
|
||||
Latitude: 0.0,
|
||||
Longitude: 0.0,
|
||||
},
|
||||
}
|
||||
|
||||
if err := c.setupClient(); err != nil {
|
||||
dbusConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := c.startSignalPump(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) Close() {
|
||||
close(c.stopChan)
|
||||
|
||||
c.sigWG.Wait()
|
||||
|
||||
if c.signals != nil {
|
||||
c.dbusConn.RemoveSignal(c.signals)
|
||||
close(c.signals)
|
||||
}
|
||||
|
||||
c.subscribers.Range(func(key string, ch chan Location) bool {
|
||||
close(ch)
|
||||
c.subscribers.Delete(key)
|
||||
return true
|
||||
})
|
||||
|
||||
if c.dbusConn != nil {
|
||||
c.dbusConn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) Subscribe(id string) chan Location {
|
||||
ch := make(chan Location, 64)
|
||||
c.subscribers.Store(id, ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) Unsubscribe(id string) {
|
||||
if ch, ok := c.subscribers.LoadAndDelete(id); ok {
|
||||
close(ch)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) setupClient() error {
|
||||
managerObj := c.dbusConn.Object(dbusGeoClueService, dbusGeoClueManagerPath)
|
||||
|
||||
if err := managerObj.Call(dbusGeoClueManagerGetClient, 0).Store(&c.clientPath); err != nil {
|
||||
return fmt.Errorf("failed to create GeoClue2 client: %w", err)
|
||||
}
|
||||
|
||||
clientObj := c.dbusConn.Object(dbusGeoClueService, c.clientPath)
|
||||
if err := clientObj.SetProperty(dbusGeoClueClientDesktopId, "dms"); err != nil {
|
||||
return fmt.Errorf("failed to set desktop ID: %w", err)
|
||||
}
|
||||
|
||||
if err := clientObj.SetProperty(dbusGeoClueClientTimeThreshold, uint(10)); err != nil {
|
||||
return fmt.Errorf("failed to set time threshold: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) startSignalPump() error {
|
||||
c.dbusConn.Signal(c.signals)
|
||||
|
||||
if err := c.dbusConn.AddMatchSignal(
|
||||
dbus.WithMatchObjectPath(c.clientPath),
|
||||
dbus.WithMatchInterface(dbusGeoClueClientInterface),
|
||||
dbus.WithMatchSender(dbusGeoClueClientLocationUpdated),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.sigWG.Add(1)
|
||||
go func() {
|
||||
defer c.sigWG.Done()
|
||||
|
||||
clientObj := c.dbusConn.Object(dbusGeoClueService, c.clientPath)
|
||||
clientObj.Call(dbusGeoClueClientTimeStart, 0)
|
||||
defer clientObj.Call(dbusGeoClueClientTimeStop, 0)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.stopChan:
|
||||
return
|
||||
case sig, ok := <-c.signals:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if sig == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
c.handleSignal(sig)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) handleSignal(sig *dbus.Signal) {
|
||||
switch sig.Name {
|
||||
case dbusGeoClueClientLocationUpdated:
|
||||
if len(sig.Body) != 2 {
|
||||
return
|
||||
}
|
||||
|
||||
newLocationPath, ok := sig.Body[1].(dbus.ObjectPath)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.handleLocationUpdated(newLocationPath); err != nil {
|
||||
log.Warn("GeoClue: Failed to handle location update: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) handleLocationUpdated(path dbus.ObjectPath) error {
|
||||
locationObj := c.dbusConn.Object(dbusGeoClueService, path)
|
||||
|
||||
lat, err := locationObj.GetProperty(dbusGeoClueLocationLatitude)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
long, err := locationObj.GetProperty(dbusGeoClueLocationLongitude)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.locationMutex.Lock()
|
||||
c.currLocation.Latitude = dbusutil.AsOr(lat, 0.0)
|
||||
c.currLocation.Longitude = dbusutil.AsOr(long, 0.0)
|
||||
c.locationMutex.Unlock()
|
||||
|
||||
c.notifySubscribers()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) notifySubscribers() {
|
||||
currentLocation, err := c.GetLocation()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.subscribers.Range(func(key string, ch chan Location) bool {
|
||||
select {
|
||||
case ch <- currentLocation:
|
||||
default:
|
||||
log.Warn("GeoClue: subscriber channel full, dropping update")
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) SeedLocation(loc Location) {
|
||||
c.locationMutex.Lock()
|
||||
defer c.locationMutex.Unlock()
|
||||
c.currLocation.Latitude = loc.Latitude
|
||||
c.currLocation.Longitude = loc.Longitude
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) GetLocation() (Location, error) {
|
||||
c.locationMutex.RLock()
|
||||
defer c.locationMutex.RUnlock()
|
||||
if c.currLocation == nil {
|
||||
return Location{
|
||||
Latitude: 0.0,
|
||||
Longitude: 0.0,
|
||||
}, nil
|
||||
}
|
||||
stateCopy := *c.currLocation
|
||||
return stateCopy, nil
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package geolocation
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type IpClient struct {
|
||||
currLocation *Location
|
||||
}
|
||||
|
||||
type ipLocationResult struct {
|
||||
Location
|
||||
City string
|
||||
}
|
||||
|
||||
type ipAPIResponse struct {
|
||||
Status string `json:"status"`
|
||||
Lat float64 `json:"lat"`
|
||||
Lon float64 `json:"lon"`
|
||||
City string `json:"city"`
|
||||
}
|
||||
|
||||
func newIpClient() *IpClient {
|
||||
return &IpClient{
|
||||
currLocation: &Location{},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *IpClient) Subscribe(id string) chan Location {
|
||||
ch := make(chan Location, 1)
|
||||
if location, err := c.GetLocation(); err == nil {
|
||||
ch <- location
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
func (c *IpClient) Unsubscribe(id string) {}
|
||||
|
||||
func (c *IpClient) Close() {}
|
||||
|
||||
func (c *IpClient) GetLocation() (Location, error) {
|
||||
if c.currLocation.Latitude != 0 || c.currLocation.Longitude != 0 {
|
||||
return *c.currLocation, nil
|
||||
}
|
||||
|
||||
result, err := fetchIPLocation()
|
||||
if err != nil {
|
||||
return Location{}, err
|
||||
}
|
||||
|
||||
c.currLocation.Latitude = result.Latitude
|
||||
c.currLocation.Longitude = result.Longitude
|
||||
return *c.currLocation, nil
|
||||
}
|
||||
|
||||
func fetchIPLocation() (ipLocationResult, error) {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
resp, err := client.Get("http://ip-api.com/json/")
|
||||
if err != nil {
|
||||
return ipLocationResult{}, fmt.Errorf("failed to fetch IP location: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return ipLocationResult{}, fmt.Errorf("ip-api.com returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return ipLocationResult{}, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
var data ipAPIResponse
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
return ipLocationResult{}, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
if data.Status == "fail" || (data.Lat == 0 && data.Lon == 0) {
|
||||
return ipLocationResult{}, fmt.Errorf("ip-api.com returned no location data")
|
||||
}
|
||||
|
||||
return ipLocationResult{
|
||||
Location: Location{Latitude: data.Lat, Longitude: data.Lon},
|
||||
City: data.City,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package geolocation
|
||||
|
||||
type Location struct {
|
||||
Latitude float64
|
||||
Longitude float64
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
GetLocation() (Location, error)
|
||||
|
||||
Subscribe(id string) chan Location
|
||||
Unsubscribe(id string)
|
||||
|
||||
Close()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,7 @@
|
||||
package matugen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
@@ -21,8 +19,6 @@ import (
|
||||
"github.com/lucasb-eyer/go-colorful"
|
||||
)
|
||||
|
||||
var ErrNoChanges = errors.New("no color changes")
|
||||
|
||||
type ColorMode string
|
||||
|
||||
const (
|
||||
@@ -58,7 +54,7 @@ var templateRegistry = []TemplateDef{
|
||||
{ID: "qt6ct", Commands: []string{"qt6ct"}, ConfigFile: "qt6ct.toml"},
|
||||
{ID: "firefox", Commands: []string{"firefox"}, ConfigFile: "firefox.toml"},
|
||||
{ID: "pywalfox", Commands: []string{"pywalfox"}, ConfigFile: "pywalfox.toml"},
|
||||
{ID: "zenbrowser", Commands: []string{"zen", "zen-browser", "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: "equibop", Commands: []string{"equibop"}, ConfigFile: "equibop.toml"},
|
||||
{ID: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal},
|
||||
@@ -164,14 +160,8 @@ func Run(opts Options) error {
|
||||
|
||||
log.Infof("Building theme: %s %s (%s)", opts.Kind, opts.Value, opts.Mode)
|
||||
|
||||
changed, buildErr := buildOnce(&opts)
|
||||
if buildErr != nil {
|
||||
return buildErr
|
||||
}
|
||||
|
||||
if !changed {
|
||||
log.Info("No color changes detected, skipping refresh")
|
||||
return ErrNoChanges
|
||||
if err := buildOnce(&opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.SyncModeWithPortal {
|
||||
@@ -182,27 +172,25 @@ func Run(opts Options) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildOnce(opts *Options) (bool, error) {
|
||||
func buildOnce(opts *Options) error {
|
||||
cfgFile, err := os.CreateTemp("", "matugen-config-*.toml")
|
||||
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 cfgFile.Close()
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "matugen-templates-*")
|
||||
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)
|
||||
|
||||
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()
|
||||
|
||||
oldColors, _ := os.ReadFile(opts.ColorsOutput())
|
||||
|
||||
var primaryDark, primaryLight, surface string
|
||||
var dank16JSON string
|
||||
var importArgs []string
|
||||
@@ -214,7 +202,7 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
surface = extractNestedColor(opts.StockColors, "surface", "dark")
|
||||
|
||||
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 == "" {
|
||||
primaryLight = primaryDark
|
||||
@@ -228,14 +216,14 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()}
|
||||
args = append(args, importArgs...)
|
||||
if err := runMatugen(args); err != nil {
|
||||
return false, err
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Infof("Using dynamic theme from %s: %s", opts.Kind, opts.Value)
|
||||
|
||||
matJSON, err := runMatugenDryRun(opts)
|
||||
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")
|
||||
@@ -243,7 +231,7 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
surface = extractMatugenColor(matJSON, "surface", "dark")
|
||||
|
||||
if primaryDark == "" {
|
||||
return false, fmt.Errorf("failed to extract primary color")
|
||||
return fmt.Errorf("failed to extract primary color")
|
||||
}
|
||||
if primaryLight == "" {
|
||||
primaryLight = primaryDark
|
||||
@@ -264,15 +252,10 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name())
|
||||
args = append(args, importArgs...)
|
||||
if err := runMatugen(args); err != nil {
|
||||
return false, err
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
newColors, _ := os.ReadFile(opts.ColorsOutput())
|
||||
if bytes.Equal(oldColors, newColors) && len(oldColors) > 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if isDMSGTKActive(opts.ConfigDir) {
|
||||
switch opts.Mode {
|
||||
case ColorModeLight:
|
||||
@@ -290,7 +273,7 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
|
||||
signalTerminals(opts)
|
||||
|
||||
return true, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
|
||||
|
||||
@@ -2,7 +2,6 @@ package matugen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
@@ -94,13 +93,10 @@ func (q *Queue) runWorker() {
|
||||
err := Run(job.Options)
|
||||
|
||||
var result Result
|
||||
switch {
|
||||
case err == nil:
|
||||
result = Result{Success: true}
|
||||
case errors.Is(err, ErrNoChanges):
|
||||
result = Result{Success: true}
|
||||
default:
|
||||
if err != nil {
|
||||
result = Result{Success: false, Error: err}
|
||||
} else {
|
||||
result = Result{Success: true}
|
||||
}
|
||||
|
||||
q.finishJob(result)
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
// Code generated by mockery v2.53.5. DO NOT EDIT.
|
||||
|
||||
package mocks_geolocation
|
||||
|
||||
import (
|
||||
geolocation "github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockClient is an autogenerated mock type for the Client type
|
||||
type MockClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockClient_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockClient) EXPECT() *MockClient_Expecter {
|
||||
return &MockClient_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Close provides a mock function with no fields
|
||||
func (_m *MockClient) Close() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// MockClient_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
|
||||
type MockClient_Close_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Close is a helper method to define mock.On call
|
||||
func (_e *MockClient_Expecter) Close() *MockClient_Close_Call {
|
||||
return &MockClient_Close_Call{Call: _e.mock.On("Close")}
|
||||
}
|
||||
|
||||
func (_c *MockClient_Close_Call) Run(run func()) *MockClient_Close_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_Close_Call) Return() *MockClient_Close_Call {
|
||||
_c.Call.Return()
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_Close_Call) RunAndReturn(run func()) *MockClient_Close_Call {
|
||||
_c.Run(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetLocation provides a mock function with no fields
|
||||
func (_m *MockClient) GetLocation() (geolocation.Location, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetLocation")
|
||||
}
|
||||
|
||||
var r0 geolocation.Location
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (geolocation.Location, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() geolocation.Location); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(geolocation.Location)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockClient_GetLocation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetLocation'
|
||||
type MockClient_GetLocation_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetLocation is a helper method to define mock.On call
|
||||
func (_e *MockClient_Expecter) GetLocation() *MockClient_GetLocation_Call {
|
||||
return &MockClient_GetLocation_Call{Call: _e.mock.On("GetLocation")}
|
||||
}
|
||||
|
||||
func (_c *MockClient_GetLocation_Call) Run(run func()) *MockClient_GetLocation_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_GetLocation_Call) Return(_a0 geolocation.Location, _a1 error) *MockClient_GetLocation_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_GetLocation_Call) RunAndReturn(run func() (geolocation.Location, error)) *MockClient_GetLocation_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Subscribe provides a mock function with given fields: id
|
||||
func (_m *MockClient) Subscribe(id string) chan geolocation.Location {
|
||||
ret := _m.Called(id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Subscribe")
|
||||
}
|
||||
|
||||
var r0 chan geolocation.Location
|
||||
if rf, ok := ret.Get(0).(func(string) chan geolocation.Location); ok {
|
||||
r0 = rf(id)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(chan geolocation.Location)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockClient_Subscribe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Subscribe'
|
||||
type MockClient_Subscribe_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Subscribe is a helper method to define mock.On call
|
||||
// - id string
|
||||
func (_e *MockClient_Expecter) Subscribe(id interface{}) *MockClient_Subscribe_Call {
|
||||
return &MockClient_Subscribe_Call{Call: _e.mock.On("Subscribe", id)}
|
||||
}
|
||||
|
||||
func (_c *MockClient_Subscribe_Call) Run(run func(id string)) *MockClient_Subscribe_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_Subscribe_Call) Return(_a0 chan geolocation.Location) *MockClient_Subscribe_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_Subscribe_Call) RunAndReturn(run func(string) chan geolocation.Location) *MockClient_Subscribe_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Unsubscribe provides a mock function with given fields: id
|
||||
func (_m *MockClient) Unsubscribe(id string) {
|
||||
_m.Called(id)
|
||||
}
|
||||
|
||||
// MockClient_Unsubscribe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Unsubscribe'
|
||||
type MockClient_Unsubscribe_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Unsubscribe is a helper method to define mock.On call
|
||||
// - id string
|
||||
func (_e *MockClient_Expecter) Unsubscribe(id interface{}) *MockClient_Unsubscribe_Call {
|
||||
return &MockClient_Unsubscribe_Call{Call: _e.mock.On("Unsubscribe", id)}
|
||||
}
|
||||
|
||||
func (_c *MockClient_Unsubscribe_Call) Run(run func(id string)) *MockClient_Unsubscribe_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_Unsubscribe_Call) Return() *MockClient_Unsubscribe_Call {
|
||||
_c.Call.Return()
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_Unsubscribe_Call) RunAndReturn(run func(string)) *MockClient_Unsubscribe_Call {
|
||||
_c.Run(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockClient creates a new instance of MockClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockClient(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockClient {
|
||||
mock := &MockClient{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -1062,62 +1062,6 @@ func (_c *MockBackend_GetWiFiNetworkDetails_Call) RunAndReturn(run func(string)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetWiFiQRCodeContent provides a mock function with given fields: ssid
|
||||
func (_m *MockBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
|
||||
ret := _m.Called(ssid)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetWiFiQRCodeContent")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (string, error)); ok {
|
||||
return rf(ssid)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) string); ok {
|
||||
r0 = rf(ssid)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(ssid)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockBackend_GetWiFiQRCodeContent_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWiFiQRCodeContent'
|
||||
type MockBackend_GetWiFiQRCodeContent_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetWiFiQRCodeContent is a helper method to define mock.On call
|
||||
// - ssid string
|
||||
func (_e *MockBackend_Expecter) GetWiFiQRCodeContent(ssid interface{}) *MockBackend_GetWiFiQRCodeContent_Call {
|
||||
return &MockBackend_GetWiFiQRCodeContent_Call{Call: _e.mock.On("GetWiFiQRCodeContent", ssid)}
|
||||
}
|
||||
|
||||
func (_c *MockBackend_GetWiFiQRCodeContent_Call) Run(run func(ssid string)) *MockBackend_GetWiFiQRCodeContent_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBackend_GetWiFiQRCodeContent_Call) Return(_a0 string, _a1 error) *MockBackend_GetWiFiQRCodeContent_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBackend_GetWiFiQRCodeContent_Call) RunAndReturn(run func(string) (string, error)) *MockBackend_GetWiFiQRCodeContent_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetWiredConnections provides a mock function with no fields
|
||||
func (_m *MockBackend) GetWiredConnections() ([]network.WiredConnection, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
@@ -311,10 +311,6 @@ func (m *Manager) handleDevicePropertiesChanged(path dbus.ObjectPath, changed ma
|
||||
select {
|
||||
case m.eventQueue <- func() {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
log.Infof("[Bluetooth] Auto-trusting newly paired device: %s", devicePath)
|
||||
if err := m.TrustDevice(devicePath, true); err != nil {
|
||||
log.Warnf("[Bluetooth] Auto-trust failed: %v", err)
|
||||
}
|
||||
log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath)
|
||||
if err := m.ConnectDevice(devicePath); err != nil {
|
||||
log.Warnf("[Bluetooth] Auto-connect failed: %v", err)
|
||||
|
||||
@@ -2,10 +2,8 @@ package cups
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -277,42 +275,13 @@ func (m *Manager) GetClasses() ([]PrinterClass, error) {
|
||||
return classes, nil
|
||||
}
|
||||
|
||||
func createPrinterViaLpadmin(name, deviceURI, ppd, information, location string) error {
|
||||
args := []string{"-p", name, "-E", "-v", deviceURI, "-m", ppd}
|
||||
if information != "" {
|
||||
args = append(args, "-D", information)
|
||||
}
|
||||
if location != "" {
|
||||
args = append(args, "-L", location)
|
||||
}
|
||||
out, err := exec.Command("lpadmin", args...).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("lpadmin failed: %s: %w", strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deletePrinterViaLpadmin(name string) error {
|
||||
out, err := exec.Command("lpadmin", "-x", name).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("lpadmin failed: %s: %w", strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) CreatePrinter(name, deviceURI, ppd string, shared bool, errorPolicy, information, location string) error {
|
||||
usedPkHelper := false
|
||||
|
||||
err := m.client.CreatePrinter(name, deviceURI, ppd, shared, errorPolicy, information, location)
|
||||
if isAuthError(err) && m.pkHelper != nil {
|
||||
if err = m.pkHelper.PrinterAdd(name, deviceURI, ppd, information, location); err != nil {
|
||||
// pkHelper failed (e.g., no polkit agent), try lpadmin as last resort.
|
||||
// lpadmin -E enables the printer, so no further setup needed.
|
||||
if lpadminErr := createPrinterViaLpadmin(name, deviceURI, ppd, information, location); lpadminErr != nil {
|
||||
return err
|
||||
}
|
||||
m.RefreshState()
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
usedPkHelper = true
|
||||
} else if err != nil {
|
||||
@@ -339,12 +308,6 @@ func (m *Manager) DeletePrinter(printerName string) error {
|
||||
err := m.client.DeletePrinter(printerName)
|
||||
if isAuthError(err) && m.pkHelper != nil {
|
||||
err = m.pkHelper.PrinterDelete(printerName)
|
||||
if err != nil {
|
||||
// pkHelper failed, try lpadmin as last resort
|
||||
if lpadminErr := deletePrinterViaLpadmin(printerName); lpadminErr == nil {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
m.RefreshState()
|
||||
|
||||
@@ -70,8 +70,6 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
||||
handleRestartJob(conn, req, manager)
|
||||
case "cups.holdJob":
|
||||
handleHoldJob(conn, req, manager)
|
||||
case "cups.testConnection":
|
||||
handleTestConnection(conn, req, manager)
|
||||
default:
|
||||
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
||||
}
|
||||
@@ -466,22 +464,3 @@ func handleHoldJob(conn net.Conn, req models.Request, manager *Manager) {
|
||||
}
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job held"})
|
||||
}
|
||||
|
||||
func handleTestConnection(conn net.Conn, req models.Request, manager *Manager) {
|
||||
host, err := params.StringNonEmpty(req.Params, "host")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
port := params.IntOpt(req.Params, "port", 631)
|
||||
protocol := params.StringOpt(req.Params, "protocol", "ipp")
|
||||
|
||||
result, err := manager.TestRemotePrinter(host, port, protocol)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, result)
|
||||
}
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
package cups
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp"
|
||||
)
|
||||
|
||||
var validProtocols = map[string]bool{
|
||||
"ipp": true,
|
||||
"ipps": true,
|
||||
"lpd": true,
|
||||
"socket": true,
|
||||
}
|
||||
|
||||
func validateTestConnectionParams(host string, port int, protocol string) error {
|
||||
if host == "" {
|
||||
return errors.New("host is required")
|
||||
}
|
||||
if strings.ContainsAny(host, " \t\n\r/\\") {
|
||||
return errors.New("host contains invalid characters")
|
||||
}
|
||||
if port < 1 || port > 65535 {
|
||||
return errors.New("port must be between 1 and 65535")
|
||||
}
|
||||
if protocol != "" && !validProtocols[protocol] {
|
||||
return errors.New("protocol must be one of: ipp, ipps, lpd, socket")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const probeTimeout = 10 * time.Second
|
||||
|
||||
func probeRemotePrinter(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
|
||||
addr := net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
||||
|
||||
// Fast fail: TCP reachability check
|
||||
conn, err := net.DialTimeout("tcp", addr, probeTimeout)
|
||||
if err != nil {
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: false,
|
||||
Error: fmt.Sprintf("cannot reach %s: %s", addr, err.Error()),
|
||||
}, nil
|
||||
}
|
||||
conn.Close()
|
||||
|
||||
// Create a temporary IPP client pointing at the remote host.
|
||||
// The TCP dial above provides fast-fail for unreachable hosts.
|
||||
// The IPP adapter's ResponseHeaderTimeout (90s) bounds stalling servers.
|
||||
client := ipp.NewIPPClient(host, port, "", "", useTLS)
|
||||
|
||||
// Try /ipp/print first (modern driverless printers), then / (legacy)
|
||||
info, err := probeIPPEndpoint(client, host, port, useTLS, "/ipp/print")
|
||||
if err != nil {
|
||||
// If we got an auth error, the printer exists but requires credentials.
|
||||
// Report it as reachable with the URI that triggered the auth challenge.
|
||||
if isAuthError(err) {
|
||||
proto := "ipp"
|
||||
if useTLS {
|
||||
proto = "ipps"
|
||||
}
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
URI: fmt.Sprintf("%s://%s:%d/ipp/print", proto, host, port),
|
||||
Info: "authentication required",
|
||||
}, nil
|
||||
}
|
||||
info, err = probeIPPEndpoint(client, host, port, useTLS, "/")
|
||||
}
|
||||
if err != nil {
|
||||
if isAuthError(err) {
|
||||
proto := "ipp"
|
||||
if useTLS {
|
||||
proto = "ipps"
|
||||
}
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
URI: fmt.Sprintf("%s://%s:%d/", proto, host, port),
|
||||
Info: "authentication required",
|
||||
}, nil
|
||||
}
|
||||
// TCP reachable but not an IPP printer
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
Error: fmt.Sprintf("host is reachable but does not appear to be an IPP printer: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func probeIPPEndpoint(client *ipp.IPPClient, host string, port int, useTLS bool, resourcePath string) (*RemotePrinterInfo, error) {
|
||||
proto := "ipp"
|
||||
if useTLS {
|
||||
proto = "ipps"
|
||||
}
|
||||
printerURI := fmt.Sprintf("%s://%s:%d%s", proto, host, port, resourcePath)
|
||||
|
||||
httpProto := "http"
|
||||
if useTLS {
|
||||
httpProto = "https"
|
||||
}
|
||||
httpURL := fmt.Sprintf("%s://%s:%d%s", httpProto, host, port, resourcePath)
|
||||
|
||||
req := ipp.NewRequest(ipp.OperationGetPrinterAttributes, 1)
|
||||
req.OperationAttributes[ipp.AttributePrinterURI] = printerURI
|
||||
req.OperationAttributes[ipp.AttributeRequestedAttributes] = []string{
|
||||
ipp.AttributePrinterName,
|
||||
ipp.AttributePrinterMakeAndModel,
|
||||
ipp.AttributePrinterState,
|
||||
ipp.AttributePrinterInfo,
|
||||
ipp.AttributePrinterUriSupported,
|
||||
}
|
||||
|
||||
resp, err := client.SendRequest(httpURL, req, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(resp.PrinterAttributes) == 0 {
|
||||
return nil, errors.New("no printer attributes returned")
|
||||
}
|
||||
|
||||
attrs := resp.PrinterAttributes[0]
|
||||
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
MakeModel: getStringAttr(attrs, ipp.AttributePrinterMakeAndModel),
|
||||
Name: getStringAttr(attrs, ipp.AttributePrinterName),
|
||||
Info: getStringAttr(attrs, ipp.AttributePrinterInfo),
|
||||
State: parsePrinterState(attrs),
|
||||
URI: printerURI,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TestRemotePrinter validates inputs and probes a remote printer via IPP.
|
||||
// For lpd/socket protocols, only TCP reachability is tested.
|
||||
func (m *Manager) TestRemotePrinter(host string, port int, protocol string) (*RemotePrinterInfo, error) {
|
||||
if protocol == "" {
|
||||
protocol = "ipp"
|
||||
}
|
||||
|
||||
if err := validateTestConnectionParams(host, port, protocol); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// For non-IPP protocols, only check TCP reachability
|
||||
if protocol == "lpd" || protocol == "socket" {
|
||||
addr := net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
||||
conn, err := net.DialTimeout("tcp", addr, probeTimeout)
|
||||
if err != nil {
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: false,
|
||||
Error: fmt.Sprintf("cannot reach %s: %s", addr, err.Error()),
|
||||
}, nil
|
||||
}
|
||||
conn.Close()
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
URI: fmt.Sprintf("%s://%s:%d", protocol, host, port),
|
||||
}, nil
|
||||
}
|
||||
|
||||
useTLS := protocol == "ipps"
|
||||
|
||||
probeFn := m.probeRemoteFn
|
||||
if probeFn == nil {
|
||||
probeFn = probeRemotePrinter
|
||||
}
|
||||
|
||||
return probeFn(host, port, useTLS)
|
||||
}
|
||||
@@ -1,397 +0,0 @@
|
||||
package cups
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestValidateTestConnectionParams(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
port int
|
||||
protocol string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "valid ipp",
|
||||
host: "192.168.0.5",
|
||||
port: 631,
|
||||
protocol: "ipp",
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "valid ipps",
|
||||
host: "printer.local",
|
||||
port: 443,
|
||||
protocol: "ipps",
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "valid lpd",
|
||||
host: "10.0.0.1",
|
||||
port: 515,
|
||||
protocol: "lpd",
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "valid socket",
|
||||
host: "10.0.0.1",
|
||||
port: 9100,
|
||||
protocol: "socket",
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "empty host",
|
||||
host: "",
|
||||
port: 631,
|
||||
protocol: "ipp",
|
||||
wantErr: "host is required",
|
||||
},
|
||||
{
|
||||
name: "port too low",
|
||||
host: "192.168.0.5",
|
||||
port: 0,
|
||||
protocol: "ipp",
|
||||
wantErr: "port must be between 1 and 65535",
|
||||
},
|
||||
{
|
||||
name: "port too high",
|
||||
host: "192.168.0.5",
|
||||
port: 70000,
|
||||
protocol: "ipp",
|
||||
wantErr: "port must be between 1 and 65535",
|
||||
},
|
||||
{
|
||||
name: "invalid protocol",
|
||||
host: "192.168.0.5",
|
||||
port: 631,
|
||||
protocol: "ftp",
|
||||
wantErr: "protocol must be one of: ipp, ipps, lpd, socket",
|
||||
},
|
||||
{
|
||||
name: "empty protocol treated as ipp",
|
||||
host: "192.168.0.5",
|
||||
port: 631,
|
||||
protocol: "",
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "host with slash",
|
||||
host: "192.168.0.5/admin",
|
||||
port: 631,
|
||||
protocol: "ipp",
|
||||
wantErr: "host contains invalid characters",
|
||||
},
|
||||
{
|
||||
name: "host with space",
|
||||
host: "192.168.0.5 ",
|
||||
port: 631,
|
||||
protocol: "ipp",
|
||||
wantErr: "host contains invalid characters",
|
||||
},
|
||||
{
|
||||
name: "host with newline",
|
||||
host: "192.168.0.5\n",
|
||||
port: 631,
|
||||
protocol: "ipp",
|
||||
wantErr: "host contains invalid characters",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateTestConnectionParams(tt.host, tt.port, tt.protocol)
|
||||
if tt.wantErr == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_TestRemotePrinter_Validation(t *testing.T) {
|
||||
m := NewTestManager(nil, nil)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
port int
|
||||
protocol string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "empty host returns error",
|
||||
host: "",
|
||||
port: 631,
|
||||
protocol: "ipp",
|
||||
wantErr: "host is required",
|
||||
},
|
||||
{
|
||||
name: "invalid port returns error",
|
||||
host: "192.168.0.5",
|
||||
port: 0,
|
||||
protocol: "ipp",
|
||||
wantErr: "port must be between 1 and 65535",
|
||||
},
|
||||
{
|
||||
name: "invalid protocol returns error",
|
||||
host: "192.168.0.5",
|
||||
port: 631,
|
||||
protocol: "ftp",
|
||||
wantErr: "protocol must be one of: ipp, ipps, lpd, socket",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := m.TestRemotePrinter(tt.host, tt.port, tt.protocol)
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_TestRemotePrinter_IPP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
protocol string
|
||||
probeRet *RemotePrinterInfo
|
||||
probeErr error
|
||||
wantTLS bool
|
||||
wantReach bool
|
||||
wantModel string
|
||||
}{
|
||||
{
|
||||
name: "successful ipp probe",
|
||||
protocol: "ipp",
|
||||
probeRet: &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
MakeModel: "HP OfficeJet 8010",
|
||||
Name: "OfficeJet",
|
||||
State: "idle",
|
||||
URI: "ipp://192.168.0.5:631/ipp/print",
|
||||
},
|
||||
wantTLS: false,
|
||||
wantReach: true,
|
||||
wantModel: "HP OfficeJet 8010",
|
||||
},
|
||||
{
|
||||
name: "successful ipps probe",
|
||||
protocol: "ipps",
|
||||
probeRet: &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
MakeModel: "HP OfficeJet 8010",
|
||||
URI: "ipps://192.168.0.5:631/ipp/print",
|
||||
},
|
||||
wantTLS: true,
|
||||
wantReach: true,
|
||||
wantModel: "HP OfficeJet 8010",
|
||||
},
|
||||
{
|
||||
name: "unreachable host",
|
||||
protocol: "ipp",
|
||||
probeRet: &RemotePrinterInfo{
|
||||
Reachable: false,
|
||||
Error: "cannot reach 192.168.0.5:631: connection refused",
|
||||
},
|
||||
wantReach: false,
|
||||
},
|
||||
{
|
||||
name: "empty protocol defaults to ipp",
|
||||
protocol: "",
|
||||
probeRet: &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
MakeModel: "Test Printer",
|
||||
},
|
||||
wantTLS: false,
|
||||
wantReach: true,
|
||||
wantModel: "Test Printer",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var capturedTLS bool
|
||||
m := NewTestManager(nil, nil)
|
||||
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
|
||||
capturedTLS = useTLS
|
||||
return tt.probeRet, tt.probeErr
|
||||
}
|
||||
|
||||
result, err := m.TestRemotePrinter("192.168.0.5", 631, tt.protocol)
|
||||
if tt.probeErr != nil {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantReach, result.Reachable)
|
||||
assert.Equal(t, tt.wantModel, result.MakeModel)
|
||||
assert.Equal(t, tt.wantTLS, capturedTLS)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_TestRemotePrinter_AuthRequired(t *testing.T) {
|
||||
m := NewTestManager(nil, nil)
|
||||
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
|
||||
// Simulate what happens when the printer returns HTTP 401
|
||||
return probeRemotePrinterWithAuthError(host, port, useTLS)
|
||||
}
|
||||
|
||||
result, err := m.TestRemotePrinter("192.168.0.107", 631, "ipp")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, result.Reachable)
|
||||
assert.Equal(t, "authentication required", result.Info)
|
||||
assert.Contains(t, result.URI, "ipp://192.168.0.107:631")
|
||||
}
|
||||
|
||||
// probeRemotePrinterWithAuthError simulates a probe where the printer
|
||||
// returns HTTP 401 on both endpoints.
|
||||
func probeRemotePrinterWithAuthError(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
|
||||
// This simulates what probeRemotePrinter does when both endpoints
|
||||
// return auth errors. We test the auth detection logic directly.
|
||||
err := ipp.HTTPError{Code: 401}
|
||||
if isAuthError(err) {
|
||||
proto := "ipp"
|
||||
if useTLS {
|
||||
proto = "ipps"
|
||||
}
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
URI: fmt.Sprintf("%s://%s:%d/ipp/print", proto, host, port),
|
||||
Info: "authentication required",
|
||||
}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func TestManager_TestRemotePrinter_NonIPPProtocol(t *testing.T) {
|
||||
m := NewTestManager(nil, nil)
|
||||
probeCalled := false
|
||||
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
|
||||
probeCalled = true
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// These will fail at TCP dial (no real server), but the important
|
||||
// thing is that probeRemoteFn is NOT called for lpd/socket.
|
||||
m.TestRemotePrinter("192.168.0.5", 9100, "socket")
|
||||
assert.False(t, probeCalled, "probe function should not be called for socket protocol")
|
||||
|
||||
m.TestRemotePrinter("192.168.0.5", 515, "lpd")
|
||||
assert.False(t, probeCalled, "probe function should not be called for lpd protocol")
|
||||
}
|
||||
|
||||
func TestHandleTestConnection_Success(t *testing.T) {
|
||||
m := NewTestManager(nil, nil)
|
||||
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
MakeModel: "HP OfficeJet 8010",
|
||||
Name: "OfficeJet",
|
||||
State: "idle",
|
||||
URI: "ipp://192.168.0.5:631/ipp/print",
|
||||
}, nil
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
conn := &mockConn{Buffer: buf}
|
||||
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
Method: "cups.testConnection",
|
||||
Params: map[string]any{
|
||||
"host": "192.168.0.5",
|
||||
},
|
||||
}
|
||||
|
||||
handleTestConnection(conn, req, m)
|
||||
|
||||
var resp models.Response[RemotePrinterInfo]
|
||||
err := json.NewDecoder(buf).Decode(&resp)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp.Result)
|
||||
assert.True(t, resp.Result.Reachable)
|
||||
assert.Equal(t, "HP OfficeJet 8010", resp.Result.MakeModel)
|
||||
}
|
||||
|
||||
func TestHandleTestConnection_MissingHost(t *testing.T) {
|
||||
m := NewTestManager(nil, nil)
|
||||
buf := &bytes.Buffer{}
|
||||
conn := &mockConn{Buffer: buf}
|
||||
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
Method: "cups.testConnection",
|
||||
Params: map[string]any{},
|
||||
}
|
||||
|
||||
handleTestConnection(conn, req, m)
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(buf).Decode(&resp)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, resp.Result)
|
||||
assert.NotNil(t, resp.Error)
|
||||
}
|
||||
|
||||
func TestHandleTestConnection_CustomPortAndProtocol(t *testing.T) {
|
||||
m := NewTestManager(nil, nil)
|
||||
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
|
||||
assert.Equal(t, 9631, port)
|
||||
assert.True(t, useTLS)
|
||||
return &RemotePrinterInfo{Reachable: true, URI: "ipps://192.168.0.5:9631/ipp/print"}, nil
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
conn := &mockConn{Buffer: buf}
|
||||
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
Method: "cups.testConnection",
|
||||
Params: map[string]any{
|
||||
"host": "192.168.0.5",
|
||||
"port": float64(9631),
|
||||
"protocol": "ipps",
|
||||
},
|
||||
}
|
||||
|
||||
handleTestConnection(conn, req, m)
|
||||
|
||||
var resp models.Response[RemotePrinterInfo]
|
||||
err := json.NewDecoder(buf).Decode(&resp)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp.Result)
|
||||
assert.True(t, resp.Result.Reachable)
|
||||
}
|
||||
|
||||
func TestHandleRequest_TestConnection(t *testing.T) {
|
||||
m := NewTestManager(nil, nil)
|
||||
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
|
||||
return &RemotePrinterInfo{Reachable: true}, nil
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
conn := &mockConn{Buffer: buf}
|
||||
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
Method: "cups.testConnection",
|
||||
Params: map[string]any{"host": "192.168.0.5"},
|
||||
}
|
||||
|
||||
HandleRequest(conn, req, m)
|
||||
|
||||
var resp models.Response[RemotePrinterInfo]
|
||||
err := json.NewDecoder(buf).Decode(&resp)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp.Result)
|
||||
assert.True(t, resp.Result.Reachable)
|
||||
}
|
||||
@@ -55,16 +55,6 @@ type PPD struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type RemotePrinterInfo struct {
|
||||
Reachable bool `json:"reachable"`
|
||||
MakeModel string `json:"makeModel"`
|
||||
Name string `json:"name"`
|
||||
Info string `json:"info"`
|
||||
State string `json:"state"`
|
||||
URI string `json:"uri"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type PrinterClass struct {
|
||||
Name string `json:"name"`
|
||||
URI string `json:"uri"`
|
||||
@@ -87,7 +77,6 @@ type Manager struct {
|
||||
notifierWg sync.WaitGroup
|
||||
lastNotifiedState *CUPSState
|
||||
baseURL string
|
||||
probeRemoteFn func(host string, port int, useTLS bool) (*RemotePrinterInfo, error)
|
||||
}
|
||||
|
||||
type SubscriptionManagerInterface interface {
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
package location
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
)
|
||||
|
||||
type LocationEvent struct {
|
||||
Type string `json:"type"`
|
||||
Data State `json:"data"`
|
||||
}
|
||||
|
||||
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
||||
switch req.Method {
|
||||
case "location.getState":
|
||||
handleGetState(conn, req, manager)
|
||||
case "location.subscribe":
|
||||
handleSubscribe(conn, req, manager)
|
||||
|
||||
default:
|
||||
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
|
||||
models.Respond(conn, req.ID, manager.GetState())
|
||||
}
|
||||
|
||||
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
|
||||
clientID := fmt.Sprintf("client-%p", conn)
|
||||
stateChan := manager.Subscribe(clientID)
|
||||
defer manager.Unsubscribe(clientID)
|
||||
|
||||
initialState := manager.GetState()
|
||||
event := LocationEvent{
|
||||
Type: "state_changed",
|
||||
Data: initialState,
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[LocationEvent]{
|
||||
ID: req.ID,
|
||||
Result: &event,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for state := range stateChan {
|
||||
event := LocationEvent{
|
||||
Type: "state_changed",
|
||||
Data: state,
|
||||
}
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[LocationEvent]{
|
||||
Result: &event,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
package location
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
func NewManager(client geolocation.Client) (*Manager, error) {
|
||||
currLocation, err := client.GetLocation()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to get initial location: %v", err)
|
||||
}
|
||||
|
||||
m := &Manager{
|
||||
client: client,
|
||||
dirty: make(chan struct{}),
|
||||
stopChan: make(chan struct{}),
|
||||
|
||||
state: &State{
|
||||
Latitude: currLocation.Latitude,
|
||||
Longitude: currLocation.Longitude,
|
||||
},
|
||||
}
|
||||
|
||||
if err := m.startSignalPump(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.notifierWg.Add(1)
|
||||
go m.notifier()
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() {
|
||||
close(m.stopChan)
|
||||
m.notifierWg.Wait()
|
||||
|
||||
m.sigWG.Wait()
|
||||
|
||||
m.subscribers.Range(func(key string, ch chan State) bool {
|
||||
close(ch)
|
||||
m.subscribers.Delete(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) Subscribe(id string) chan State {
|
||||
ch := make(chan State, 64)
|
||||
m.subscribers.Store(id, ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
func (m *Manager) Unsubscribe(id string) {
|
||||
if ch, ok := m.subscribers.LoadAndDelete(id); ok {
|
||||
close(ch)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) startSignalPump() error {
|
||||
m.sigWG.Add(1)
|
||||
go func() {
|
||||
defer m.sigWG.Done()
|
||||
|
||||
subscription := m.client.Subscribe("locationManager")
|
||||
defer m.client.Unsubscribe("locationManager")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-m.stopChan:
|
||||
return
|
||||
case location, ok := <-subscription:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
m.handleLocationChange(location)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) handleLocationChange(location geolocation.Location) {
|
||||
m.stateMutex.Lock()
|
||||
defer m.stateMutex.Unlock()
|
||||
|
||||
m.state.Latitude = location.Latitude
|
||||
m.state.Longitude = location.Longitude
|
||||
|
||||
m.notifySubscribers()
|
||||
}
|
||||
|
||||
func (m *Manager) notifySubscribers() {
|
||||
select {
|
||||
case m.dirty <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) GetState() State {
|
||||
m.stateMutex.RLock()
|
||||
defer m.stateMutex.RUnlock()
|
||||
if m.state == nil {
|
||||
return State{
|
||||
Latitude: 0.0,
|
||||
Longitude: 0.0,
|
||||
}
|
||||
}
|
||||
stateCopy := *m.state
|
||||
return stateCopy
|
||||
}
|
||||
|
||||
func (m *Manager) notifier() {
|
||||
defer m.notifierWg.Done()
|
||||
const minGap = 200 * time.Millisecond
|
||||
timer := time.NewTimer(minGap)
|
||||
timer.Stop()
|
||||
var pending bool
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-m.stopChan:
|
||||
timer.Stop()
|
||||
return
|
||||
case <-m.dirty:
|
||||
if pending {
|
||||
continue
|
||||
}
|
||||
pending = true
|
||||
timer.Reset(minGap)
|
||||
case <-timer.C:
|
||||
if !pending {
|
||||
continue
|
||||
}
|
||||
|
||||
currentState := m.GetState()
|
||||
|
||||
if m.lastNotified != nil && !stateChanged(m.lastNotified, ¤tState) {
|
||||
pending = false
|
||||
continue
|
||||
}
|
||||
|
||||
m.subscribers.Range(func(key string, ch chan State) bool {
|
||||
select {
|
||||
case ch <- currentState:
|
||||
default:
|
||||
log.Warn("Location: subscriber channel full, dropping update")
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
stateCopy := currentState
|
||||
m.lastNotified = &stateCopy
|
||||
pending = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stateChanged(old, new *State) bool {
|
||||
if old == nil || new == nil {
|
||||
return true
|
||||
}
|
||||
if old.Latitude != new.Latitude {
|
||||
return true
|
||||
}
|
||||
if old.Longitude != new.Longitude {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package location
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
)
|
||||
|
||||
type State struct {
|
||||
Latitude float64 `json:"latitude"`
|
||||
Longitude float64 `json:"longitude"`
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
state *State
|
||||
stateMutex sync.RWMutex
|
||||
|
||||
client geolocation.Client
|
||||
|
||||
stopChan chan struct{}
|
||||
sigWG sync.WaitGroup
|
||||
|
||||
subscribers syncmap.Map[string, chan State]
|
||||
dirty chan struct{}
|
||||
notifierWg sync.WaitGroup
|
||||
lastNotified *State
|
||||
}
|
||||
@@ -5,6 +5,5 @@ const (
|
||||
dbusPath = "/org/freedesktop/login1"
|
||||
dbusManagerInterface = "org.freedesktop.login1.Manager"
|
||||
dbusSessionInterface = "org.freedesktop.login1.Session"
|
||||
dbusUserInterface = "org.freedesktop.login1.User"
|
||||
dbusPropsInterface = "org.freedesktop.DBus.Properties"
|
||||
)
|
||||
|
||||
@@ -17,8 +17,15 @@ func NewManager() (*Manager, error) {
|
||||
return nil, fmt.Errorf("failed to connect to system bus: %w", err)
|
||||
}
|
||||
|
||||
sessionID := os.Getenv("XDG_SESSION_ID")
|
||||
if sessionID == "" {
|
||||
sessionID = "self"
|
||||
}
|
||||
|
||||
m := &Manager{
|
||||
state: &SessionState{},
|
||||
state: &SessionState{
|
||||
SessionID: sessionID,
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
|
||||
stopChan: make(chan struct{}),
|
||||
@@ -53,13 +60,12 @@ func (m *Manager) initialize() error {
|
||||
|
||||
m.initializeFallbackDelay()
|
||||
|
||||
sessionID, sessionPath, err := m.discoverSession()
|
||||
sessionPath, err := m.getSession(m.state.SessionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get session path: %w", err)
|
||||
}
|
||||
|
||||
m.stateMutex.Lock()
|
||||
m.state.SessionID = sessionID
|
||||
m.state.SessionPath = string(sessionPath)
|
||||
m.sessionPath = sessionPath
|
||||
m.stateMutex.Unlock()
|
||||
@@ -73,41 +79,6 @@ func (m *Manager) initialize() error {
|
||||
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) {
|
||||
var out dbus.ObjectPath
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
if m.managerObj == nil || m.conn == nil {
|
||||
return fmt.Errorf("manager not fully initialized")
|
||||
|
||||
@@ -10,7 +10,6 @@ type Backend interface {
|
||||
ScanWiFi() error
|
||||
ScanWiFiDevice(device string) error
|
||||
GetWiFiNetworkDetails(ssid string) (*NetworkInfoResponse, error)
|
||||
GetWiFiQRCodeContent(ssid string) (string, error)
|
||||
GetWiFiDevices() []WiFiDevice
|
||||
|
||||
ConnectWiFi(req ConnectionRequest) error
|
||||
|
||||
@@ -111,10 +111,6 @@ func (b *HybridIwdNetworkdBackend) GetWiFiNetworkDetails(ssid string) (*NetworkI
|
||||
return b.wifi.GetWiFiNetworkDetails(ssid)
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
|
||||
return b.wifi.GetWiFiQRCodeContent(ssid)
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) ConnectWiFi(req ConnectionRequest) error {
|
||||
if err := b.wifi.ConnectWiFi(req); err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
import "fmt"
|
||||
|
||||
func (b *IWDBackend) GetWiredConnections() ([]WiredConnection, error) {
|
||||
return nil, fmt.Errorf("wired connections not supported by iwd")
|
||||
@@ -115,19 +112,3 @@ func (b *IWDBackend) getWiFiDevicesLocked() []WiFiDevice {
|
||||
Networks: b.state.WiFiNetworks,
|
||||
}}
|
||||
}
|
||||
|
||||
func (b *IWDBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
|
||||
path := iwdConfigPath(ssid)
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("no saved iwd config for `%s`: %w", ssid, err)
|
||||
}
|
||||
|
||||
passphrase, err := parseIWDPassphrase(string(data))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read passphrase for `%s`: %w", ssid, err)
|
||||
}
|
||||
|
||||
return FormatWiFiQRString("WPA", ssid, passphrase), nil
|
||||
}
|
||||
|
||||
@@ -18,10 +18,6 @@ func (b *SystemdNetworkdBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInf
|
||||
return nil, fmt.Errorf("WiFi details not supported by networkd backend")
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
|
||||
return "", fmt.Errorf("WiFi QR Code not supported by networkd backend")
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) ConnectWiFi(req ConnectionRequest) error {
|
||||
return fmt.Errorf("WiFi connect not supported by networkd backend")
|
||||
}
|
||||
|
||||
@@ -196,65 +196,6 @@ func (b *NetworkManagerBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInfo
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
|
||||
conn, err := b.findConnection(ssid)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("no saved connection for `%s`: %w", ssid, err)
|
||||
}
|
||||
|
||||
connSettings, err := conn.GetSettings()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get settings for `%s`: %w", ssid, err)
|
||||
}
|
||||
|
||||
secSettings, ok := connSettings["802-11-wireless-security"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("network `%s` has no security settings", ssid)
|
||||
}
|
||||
|
||||
keyMgmt, ok := secSettings["key-mgmt"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("failed to identify security type of network `%s`", ssid)
|
||||
}
|
||||
|
||||
var securityType string
|
||||
switch keyMgmt {
|
||||
case "none":
|
||||
authAlg, _ := secSettings["auth-alg"].(string)
|
||||
switch authAlg {
|
||||
case "open":
|
||||
securityType = "nopass"
|
||||
default:
|
||||
securityType = "WEP"
|
||||
}
|
||||
case "ieee8021x":
|
||||
securityType = "WEP"
|
||||
default:
|
||||
securityType = "WPA"
|
||||
}
|
||||
|
||||
if securityType != "WPA" {
|
||||
return "", fmt.Errorf("QR code generation only supports WPA connections, `%s` uses %s", ssid, securityType)
|
||||
}
|
||||
|
||||
secrets, err := conn.GetSecrets("802-11-wireless-security")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to retrieve connection secrets for `%s`: %w", ssid, err)
|
||||
}
|
||||
|
||||
secSecrets, ok := secrets["802-11-wireless-security"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("failed to retrieve password for `%s`", ssid)
|
||||
}
|
||||
|
||||
psk, ok := secSecrets["psk"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("failed to retrieve password for `%s`", ssid)
|
||||
}
|
||||
|
||||
return FormatWiFiQRString(securityType, ssid, psk), nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) ConnectWiFi(req ConnectionRequest) error {
|
||||
devInfo, err := b.getWifiDeviceForConnection(req.Device)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -29,13 +28,7 @@ func TestDetectResult_HasNetworkdField(t *testing.T) {
|
||||
|
||||
func TestDetectNetworkStack_Integration(t *testing.T) {
|
||||
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)
|
||||
if assert.NotNil(t, result) {
|
||||
assert.NotEmpty(t, result.ChosenReason)
|
||||
}
|
||||
assert.NotNil(t, result)
|
||||
assert.NotEmpty(t, result.ChosenReason)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
@@ -41,10 +40,6 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
||||
handleSetPreference(conn, req, manager)
|
||||
case "network.info":
|
||||
handleGetNetworkInfo(conn, req, manager)
|
||||
case "network.qrcode":
|
||||
handleGetNetworkQRCode(conn, req, manager)
|
||||
case "network.delete-qrcode":
|
||||
handleDeleteQRCode(conn, req, manager)
|
||||
case "network.ethernet.info":
|
||||
handleGetWiredNetworkInfo(conn, req, manager)
|
||||
case "network.subscribe":
|
||||
@@ -325,42 +320,6 @@ func handleGetNetworkInfo(conn net.Conn, req models.Request, manager *Manager) {
|
||||
models.Respond(conn, req.ID, network)
|
||||
}
|
||||
|
||||
func handleGetNetworkQRCode(conn net.Conn, req models.Request, manager *Manager) {
|
||||
ssid, err := params.String(req.Params, "ssid")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
content, err := manager.GetNetworkQRCode(ssid)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, content)
|
||||
}
|
||||
|
||||
func handleDeleteQRCode(conn net.Conn, req models.Request, _ *Manager) {
|
||||
path, err := params.String(req.Params, "path")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !isValidQRCodePath(path) {
|
||||
models.RespondError(conn, req.ID, "invalid QR code path")
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Remove(path); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "QR code file deleted"})
|
||||
}
|
||||
|
||||
func handleGetWiredNetworkInfo(conn net.Conn, req models.Request, manager *Manager) {
|
||||
uuid, err := params.String(req.Params, "uuid")
|
||||
if err != nil {
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/yeqown/go-qrcode/v2"
|
||||
"github.com/yeqown/go-qrcode/writer/standard"
|
||||
)
|
||||
|
||||
func NewManager() (*Manager, error) {
|
||||
@@ -440,43 +438,6 @@ func (m *Manager) GetNetworkInfoDetailed(ssid string) (*NetworkInfoResponse, err
|
||||
return m.backend.GetWiFiNetworkDetails(ssid)
|
||||
}
|
||||
|
||||
func (m *Manager) GetNetworkQRCode(ssid string) ([2]string, error) {
|
||||
content, err := m.backend.GetWiFiQRCodeContent(ssid)
|
||||
if err != nil {
|
||||
return [2]string{}, err
|
||||
}
|
||||
|
||||
qrc, err := qrcode.New(content)
|
||||
if err != nil {
|
||||
return [2]string{}, fmt.Errorf("failed to create QR code for `%s`: %w", ssid, err)
|
||||
}
|
||||
|
||||
pathThemed, pathNormal := qrCodePaths(ssid)
|
||||
|
||||
wThemed, err := standard.New(
|
||||
pathThemed,
|
||||
standard.WithBuiltinImageEncoder(standard.PNG_FORMAT),
|
||||
standard.WithBgTransparent(),
|
||||
standard.WithFgColorRGBHex("#ffffff"),
|
||||
)
|
||||
if err != nil {
|
||||
return [2]string{}, fmt.Errorf("failed to create QR code writer: %w", err)
|
||||
}
|
||||
if err := qrc.Save(wThemed); err != nil {
|
||||
return [2]string{}, fmt.Errorf("failed to save QR code for `%s`: %w", ssid, err)
|
||||
}
|
||||
|
||||
wNormal, err := standard.New(pathNormal, standard.WithBuiltinImageEncoder(standard.PNG_FORMAT))
|
||||
if err != nil {
|
||||
return [2]string{}, fmt.Errorf("failed to create QR code writer: %w", err)
|
||||
}
|
||||
if err := qrc.Save(wNormal); err != nil {
|
||||
return [2]string{}, fmt.Errorf("failed to save QR code for `%s`: %w", ssid, err)
|
||||
}
|
||||
|
||||
return [2]string{pathThemed, pathNormal}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) ToggleWiFi() error {
|
||||
enabled, err := m.backend.GetWiFiEnabled()
|
||||
if err != nil {
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const qrCodeTmpPrefix = "/tmp/dank-wifi-qrcode-"
|
||||
|
||||
func FormatWiFiQRString(securityType, ssid, password string) string {
|
||||
return fmt.Sprintf("WIFI:T:%s;S:%s;P:%s;;", securityType, ssid, password)
|
||||
}
|
||||
|
||||
func qrCodePaths(ssid string) (themed, normal string) {
|
||||
safe := sanitizeSSIDForPath(ssid)
|
||||
themed = fmt.Sprintf("%s%s-themed.png", qrCodeTmpPrefix, safe)
|
||||
normal = fmt.Sprintf("%s%s-normal.png", qrCodeTmpPrefix, safe)
|
||||
return
|
||||
}
|
||||
|
||||
func isValidQRCodePath(path string) bool {
|
||||
clean := filepath.Clean(path)
|
||||
return strings.HasPrefix(clean, qrCodeTmpPrefix) && strings.HasSuffix(clean, ".png")
|
||||
}
|
||||
|
||||
var safePathChar = regexp.MustCompile(`[^a-zA-Z0-9_-]`)
|
||||
|
||||
func sanitizeSSIDForPath(ssid string) string {
|
||||
return safePathChar.ReplaceAllString(ssid, "_")
|
||||
}
|
||||
|
||||
var iwdVerbatimSSID = regexp.MustCompile(`^[a-zA-Z0-9 _-]+$`)
|
||||
|
||||
func iwdConfigPath(ssid string) string {
|
||||
switch {
|
||||
case iwdVerbatimSSID.MatchString(ssid):
|
||||
return fmt.Sprintf("/var/lib/iwd/%s.psk", ssid)
|
||||
default:
|
||||
return fmt.Sprintf("/var/lib/iwd/=%x.psk", []byte(ssid))
|
||||
}
|
||||
}
|
||||
|
||||
func parseIWDPassphrase(data string) (string, error) {
|
||||
inSecurity := false
|
||||
for _, line := range strings.Split(data, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
switch {
|
||||
case line == "[Security]":
|
||||
inSecurity = true
|
||||
case strings.HasPrefix(line, "["):
|
||||
inSecurity = false
|
||||
case inSecurity && strings.HasPrefix(line, "Passphrase="):
|
||||
return strings.TrimPrefix(line, "Passphrase="), nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no passphrase found in iwd config")
|
||||
}
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/location"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
||||
@@ -193,15 +192,6 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(req.Method, "location.") {
|
||||
if locationManager == nil {
|
||||
models.RespondError(conn, req.ID, "location manager not initialized")
|
||||
return
|
||||
}
|
||||
location.HandleRequest(conn, req, locationManager)
|
||||
return
|
||||
}
|
||||
|
||||
switch req.Method {
|
||||
case "ping":
|
||||
models.Respond(conn, req.ID, "pong")
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/apppicker"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/bluez"
|
||||
@@ -26,7 +25,6 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/location"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
||||
@@ -72,7 +70,6 @@ var clipboardManager *clipboard.Manager
|
||||
var dbusManager *serverDbus.Manager
|
||||
var wlContext *wlcontext.SharedContext
|
||||
var themeModeManager *thememode.Manager
|
||||
var locationManager *location.Manager
|
||||
|
||||
const dbusClientID = "dms-dbus-client"
|
||||
|
||||
@@ -191,7 +188,7 @@ func InitializeFreedeskManager() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitializeWaylandManager(geoClient geolocation.Client) error {
|
||||
func InitializeWaylandManager() error {
|
||||
log.Info("Attempting to initialize Wayland gamma control...")
|
||||
|
||||
if wlContext == nil {
|
||||
@@ -204,7 +201,7 @@ func InitializeWaylandManager(geoClient geolocation.Client) error {
|
||||
}
|
||||
|
||||
config := wayland.DefaultConfig()
|
||||
manager, err := wayland.NewManager(wlContext.Display(), geoClient, config)
|
||||
manager, err := wayland.NewManager(wlContext.Display(), config)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to initialize wayland manager: %v", err)
|
||||
return err
|
||||
@@ -385,27 +382,14 @@ func InitializeDbusManager() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitializeThemeModeManager(geoClient geolocation.Client) error {
|
||||
manager := thememode.NewManager(geoClient)
|
||||
func InitializeThemeModeManager() error {
|
||||
manager := thememode.NewManager()
|
||||
themeModeManager = manager
|
||||
|
||||
log.Info("Theme mode automation manager initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitializeLocationManager(geoClient geolocation.Client) error {
|
||||
manager, err := location.NewManager(geoClient)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to initialize location manager: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
locationManager = manager
|
||||
|
||||
log.Info("Location manager initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleConnection(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
@@ -553,10 +537,6 @@ func getServerInfo() ServerInfo {
|
||||
caps = append(caps, "theme.auto")
|
||||
}
|
||||
|
||||
if locationManager != nil {
|
||||
caps = append(caps, "location")
|
||||
}
|
||||
|
||||
if dbusManager != nil {
|
||||
caps = append(caps, "dbus")
|
||||
}
|
||||
@@ -1327,9 +1307,6 @@ func cleanupManagers() {
|
||||
if wlContext != nil {
|
||||
wlContext.Close()
|
||||
}
|
||||
if locationManager != nil {
|
||||
locationManager.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func Start(printDocs bool) error {
|
||||
@@ -1511,9 +1488,6 @@ func Start(printDocs bool) error {
|
||||
log.Info(" clipboard.getConfig - Get clipboard configuration")
|
||||
log.Info(" clipboard.setConfig - Set configuration (params: maxHistory?, maxEntrySize?, autoClearDays?, clearAtStartup?)")
|
||||
log.Info(" clipboard.subscribe - Subscribe to clipboard state changes (streaming)")
|
||||
log.Info("Location:")
|
||||
log.Info(" location.getState - Get current location state")
|
||||
log.Info(" location.subscribe - Subscribe to location changes (streaming)")
|
||||
log.Info("")
|
||||
}
|
||||
log.Info("Initializing managers...")
|
||||
@@ -1545,9 +1519,6 @@ func Start(printDocs bool) error {
|
||||
loginctlReady := make(chan struct{})
|
||||
freedesktopReady := make(chan struct{})
|
||||
|
||||
geoClient := geolocation.NewClient()
|
||||
defer geoClient.Close()
|
||||
|
||||
go func() {
|
||||
defer close(loginctlReady)
|
||||
if err := InitializeLoginctlManager(); err != nil {
|
||||
@@ -1592,7 +1563,7 @@ func Start(printDocs bool) error {
|
||||
}
|
||||
}()
|
||||
|
||||
if err := InitializeWaylandManager(geoClient); err != nil {
|
||||
if err := InitializeWaylandManager(); err != nil {
|
||||
log.Warnf("Wayland manager unavailable: %v", err)
|
||||
}
|
||||
|
||||
@@ -1624,21 +1595,8 @@ func Start(printDocs bool) error {
|
||||
log.Debugf("WlrOutput manager unavailable: %v", err)
|
||||
}
|
||||
|
||||
if err := InitializeThemeModeManager(geoClient); err != nil {
|
||||
if err := InitializeThemeModeManager(); err != nil {
|
||||
log.Warnf("Theme mode manager unavailable: %v", err)
|
||||
} else {
|
||||
notifyCapabilityChange()
|
||||
go func() {
|
||||
<-loginctlReady
|
||||
if loginctlManager == nil {
|
||||
return
|
||||
}
|
||||
themeModeManager.WatchLoginctl(loginctlManager)
|
||||
}()
|
||||
}
|
||||
|
||||
if err := InitializeLocationManager(geoClient); err != nil {
|
||||
log.Warnf("Location manager unavailable: %v", err)
|
||||
} else {
|
||||
notifyCapabilityChange()
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
)
|
||||
@@ -33,14 +31,12 @@ type Manager struct {
|
||||
cachedIPLat *float64
|
||||
cachedIPLon *float64
|
||||
|
||||
geoClient geolocation.Client
|
||||
|
||||
stopChan chan struct{}
|
||||
updateTrigger chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewManager(geoClient geolocation.Client) *Manager {
|
||||
func NewManager() *Manager {
|
||||
m := &Manager{
|
||||
config: Config{
|
||||
Enabled: false,
|
||||
@@ -54,7 +50,6 @@ func NewManager(geoClient geolocation.Client) *Manager {
|
||||
},
|
||||
stopChan: make(chan struct{}),
|
||||
updateTrigger: make(chan struct{}, 1),
|
||||
geoClient: geoClient,
|
||||
}
|
||||
|
||||
m.updateState(time.Now())
|
||||
@@ -192,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() {
|
||||
defer m.wg.Done()
|
||||
|
||||
@@ -331,17 +303,17 @@ func (m *Manager) getLocation(config Config) (*float64, *float64) {
|
||||
}
|
||||
m.locationMutex.RUnlock()
|
||||
|
||||
location, err := m.geoClient.GetLocation()
|
||||
lat, lon, err := wayland.FetchIPLocation()
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
m.locationMutex.Lock()
|
||||
m.cachedIPLat = &location.Latitude
|
||||
m.cachedIPLon = &location.Longitude
|
||||
m.cachedIPLat = lat
|
||||
m.cachedIPLon = lon
|
||||
m.locationMutex.Unlock()
|
||||
|
||||
return m.cachedIPLat, m.cachedIPLon
|
||||
return lat, lon
|
||||
}
|
||||
|
||||
func statesEqual(a, b *State) bool {
|
||||
@@ -355,12 +327,10 @@ func statesEqual(a, b *State) bool {
|
||||
}
|
||||
|
||||
func (m *Manager) computeSchedule(now time.Time, config Config) (bool, time.Time) {
|
||||
switch config.Mode {
|
||||
case "location":
|
||||
if config.Mode == "location" {
|
||||
return m.computeLocationSchedule(now, config)
|
||||
default:
|
||||
return computeTimeSchedule(now, config)
|
||||
}
|
||||
return computeTimeSchedule(now, config)
|
||||
}
|
||||
|
||||
func computeTimeSchedule(now time.Time, config Config) (bool, time.Time) {
|
||||
@@ -411,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)
|
||||
switch cond {
|
||||
case wayland.SunMidnightSun:
|
||||
return true, startOfNextDay(now)
|
||||
case wayland.SunPolarNight:
|
||||
if cond != wayland.SunNormal {
|
||||
if cond == wayland.SunMidnightSun {
|
||||
return true, startOfNextDay(now)
|
||||
}
|
||||
return false, startOfNextDay(now)
|
||||
}
|
||||
|
||||
@@ -427,10 +397,10 @@ func (m *Manager) computeLocationSchedule(now time.Time, config Config) (bool, t
|
||||
|
||||
nextDay := startOfNextDay(now)
|
||||
nextTimes, nextCond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, nextDay, config.ElevationTwilight, config.ElevationDaylight)
|
||||
switch nextCond {
|
||||
case wayland.SunMidnightSun:
|
||||
return true, startOfNextDay(nextDay)
|
||||
case wayland.SunPolarNight:
|
||||
if nextCond != wayland.SunNormal {
|
||||
if nextCond == wayland.SunMidnightSun {
|
||||
return true, startOfNextDay(nextDay)
|
||||
}
|
||||
return false, startOfNextDay(nextDay)
|
||||
}
|
||||
|
||||
@@ -443,7 +413,13 @@ func startOfNextDay(t time.Time) time.Time {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -13,14 +13,13 @@ import (
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_gamma_control"
|
||||
)
|
||||
|
||||
const animKelvinStep = 25
|
||||
|
||||
func NewManager(display wlclient.WaylandDisplay, geoClient geolocation.Client, config Config) (*Manager, error) {
|
||||
func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error) {
|
||||
if err := config.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -41,7 +40,6 @@ func NewManager(display wlclient.WaylandDisplay, geoClient geolocation.Client, c
|
||||
updateTrigger: make(chan struct{}, 1),
|
||||
dirty: make(chan struct{}, 1),
|
||||
dbusSignal: make(chan *dbus.Signal, 16),
|
||||
geoClient: geoClient,
|
||||
}
|
||||
|
||||
if err := m.setupRegistry(); err != nil {
|
||||
@@ -439,16 +437,15 @@ func (m *Manager) getLocation() (*float64, *float64) {
|
||||
}
|
||||
m.locationMutex.RUnlock()
|
||||
|
||||
location, err := m.geoClient.GetLocation()
|
||||
lat, lon, err := FetchIPLocation()
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
m.locationMutex.Lock()
|
||||
m.cachedIPLat = &location.Latitude
|
||||
m.cachedIPLon = &location.Longitude
|
||||
m.cachedIPLat = lat
|
||||
m.cachedIPLon = lon
|
||||
m.locationMutex.Unlock()
|
||||
return m.cachedIPLat, m.cachedIPLon
|
||||
return lat, lon
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
mocks_geolocation "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/geolocation"
|
||||
mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient"
|
||||
)
|
||||
|
||||
@@ -391,20 +390,18 @@ func TestNotifySubscribers_NonBlocking(t *testing.T) {
|
||||
|
||||
func TestNewManager_GetRegistryError(t *testing.T) {
|
||||
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
|
||||
mockGeoclient := mocks_geolocation.NewMockClient(t)
|
||||
|
||||
mockDisplay.EXPECT().Context().Return(nil)
|
||||
mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry"))
|
||||
|
||||
config := DefaultConfig()
|
||||
_, err := NewManager(mockDisplay, mockGeoclient, config)
|
||||
_, err := NewManager(mockDisplay, config)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "get registry")
|
||||
}
|
||||
|
||||
func TestNewManager_InvalidConfig(t *testing.T) {
|
||||
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
|
||||
mockGeoclient := mocks_geolocation.NewMockClient(t)
|
||||
|
||||
config := Config{
|
||||
LowTemp: 500,
|
||||
@@ -412,6 +409,6 @@ func TestNewManager_InvalidConfig(t *testing.T) {
|
||||
Gamma: 1.0,
|
||||
}
|
||||
|
||||
_, err := NewManager(mockDisplay, mockGeoclient, config)
|
||||
_, err := NewManager(mockDisplay, config)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
"github.com/godbus/dbus/v5"
|
||||
@@ -98,8 +97,6 @@ type Manager struct {
|
||||
dbusConn *dbus.Conn
|
||||
dbusSignal chan *dbus.Signal
|
||||
|
||||
geoClient geolocation.Client
|
||||
|
||||
lastAppliedTemp int
|
||||
lastAppliedGamma float64
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
if i == m.selectedDep {
|
||||
line = fmt.Sprintf("▶ %s%s%-25s %s", reinstallMarker, variantMarker, dep.Name, status)
|
||||
if dep.Version != "" {
|
||||
line += fmt.Sprintf(" (%s)", dep.Version)
|
||||
}
|
||||
line = m.styles.SelectedOption.Render(line) + note
|
||||
line = m.styles.SelectedOption.Render(line)
|
||||
} else {
|
||||
line = fmt.Sprintf(" %s%s%-25s %s", reinstallMarker, variantMarker, dep.Name, status)
|
||||
if dep.Version != "" {
|
||||
line += fmt.Sprintf(" (%s)", dep.Version)
|
||||
}
|
||||
line = m.styles.Normal.Render(line) + note
|
||||
line = m.styles.Normal.Render(line)
|
||||
}
|
||||
|
||||
b.WriteString(line)
|
||||
@@ -121,13 +115,6 @@ func (m Model) updateDetectingDepsState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.state = StateError
|
||||
} else {
|
||||
m.dependencies = depsMsg.deps
|
||||
// dms-greeter is opt-in skipped by default
|
||||
for _, dep := range depsMsg.deps {
|
||||
if dep.Name == "dms-greeter" {
|
||||
m.disabledItems["dms-greeter"] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
m.state = StateDependencyReview
|
||||
}
|
||||
return m, m.listenForLogs()
|
||||
@@ -243,41 +230,6 @@ func (m Model) installPackages() tea.Cmd {
|
||||
// Convert installer messages to TUI messages
|
||||
go func() {
|
||||
for msg := range installerProgressChan {
|
||||
// Run optional greeter setup
|
||||
if msg.Phase == distros.PhaseComplete && msg.IsComplete && msg.Error == nil {
|
||||
greeterSelected := false
|
||||
for _, dep := range m.dependencies {
|
||||
if dep.Name == "dms-greeter" && !m.disabledItems["dms-greeter"] {
|
||||
greeterSelected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if greeterSelected {
|
||||
compositorName := "niri"
|
||||
if m.selectedWM == 1 {
|
||||
compositorName = "Hyprland"
|
||||
}
|
||||
m.packageProgressChan <- packageInstallProgressMsg{
|
||||
progress: 0.92,
|
||||
step: "Configuring DMS greeter...",
|
||||
logOutput: "Starting automated greeter setup...",
|
||||
}
|
||||
greeterLogFunc := func(line string) {
|
||||
m.packageProgressChan <- packageInstallProgressMsg{
|
||||
progress: 0.94,
|
||||
step: "Configuring DMS greeter...",
|
||||
logOutput: line,
|
||||
}
|
||||
}
|
||||
if err := greeter.AutoSetupGreeter(compositorName, m.sudoPassword, greeterLogFunc); err != nil {
|
||||
m.packageProgressChan <- packageInstallProgressMsg{
|
||||
progress: 0.96,
|
||||
step: "Greeter setup warning",
|
||||
logOutput: fmt.Sprintf("⚠ Greeter auto-setup warning (non-fatal): %v", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tuiMsg := packageInstallProgressMsg{
|
||||
progress: msg.Progress,
|
||||
step: msg.Step,
|
||||
|
||||
@@ -864,12 +864,10 @@ func (p *NiriWritableProvider) formatRule(rule windowrules.WindowRule) string {
|
||||
|
||||
func formatSizeProperty(name, value string) string {
|
||||
parts := strings.SplitN(value, " ", 2)
|
||||
if len(parts) == 2 {
|
||||
return fmt.Sprintf(" %s { %s %s; }", name, parts[0], parts[1])
|
||||
if len(parts) != 2 {
|
||||
return fmt.Sprintf(" %s { }", name)
|
||||
}
|
||||
// Bare number without type prefix — default to "fixed"
|
||||
if _, err := strconv.Atoi(value); err == nil {
|
||||
return fmt.Sprintf(" %s { fixed %s; }", name, value)
|
||||
}
|
||||
return fmt.Sprintf(" %s { }", name)
|
||||
sizeType := parts[0]
|
||||
sizeValue := parts[1]
|
||||
return fmt.Sprintf(" %s { %s %s; }", name, sizeType, sizeValue)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ Depends: ${misc:Depends},
|
||||
qml6-module-qtquick-templates,
|
||||
qml6-module-qtquick-window,
|
||||
qt6ct
|
||||
Suggests: cups-pk-helper
|
||||
Provides: dms
|
||||
Conflicts: dms
|
||||
Replaces: dms
|
||||
|
||||
@@ -27,12 +27,12 @@ override_dh_auto_build:
|
||||
# Verify core directory exists (native package format has source at root)
|
||||
test -d core || (echo "ERROR: core directory not found!" && exit 1)
|
||||
|
||||
# Pin go.mod and vendor/modules.txt to the installed Go toolchain version
|
||||
GO_INSTALLED=$$(go version | grep -oP 'go\K[0-9]+\.[0-9]+'); \
|
||||
sed -i "s/^go [0-9]\+\.[0-9]\+\(\.[0-9]*\)\?$$/go $${GO_INSTALLED}/" core/go.mod; \
|
||||
sed -i "s/^\(## explicit; go \)[0-9]\+\.[0-9]\+\(\.[0-9]*\)\?$$/\1$${GO_INSTALLED}/" core/vendor/modules.txt
|
||||
# Patch go.mod to use Go 1.24 base version (Debian 13 has 1.23.x, may vary)
|
||||
sed -i 's/^go 1\.24\.[0-9]*/go 1.24/' core/go.mod
|
||||
|
||||
# Build dms-cli (single shell to preserve variables; arch: Debian amd64/arm64 -> Makefile amd64/arm64)
|
||||
# Build dms-cli from source using vendored dependencies
|
||||
# Extract version info and build in single shell to preserve variables
|
||||
# Architecture mapping: Debian amd64/arm64 -> Makefile amd64/arm64
|
||||
VERSION="$(UPSTREAM_VERSION)"; \
|
||||
COMMIT=$$(echo "$(UPSTREAM_VERSION)" | grep -oP '(?<=git)[0-9]+\.[a-f0-9]+' | cut -d. -f2 | head -c8 || echo "unknown"); \
|
||||
if [ "$(DEB_HOST_ARCH)" = "amd64" ]; then \
|
||||
|
||||
@@ -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.3/dms-qml.tar.gz</param>
|
||||
<param name="filename">dms-qml.tar.gz</param>
|
||||
</service>
|
||||
</services>
|
||||
@@ -1,5 +0,0 @@
|
||||
dms-greeter (1.4.3db1) unstable; urgency=medium
|
||||
|
||||
* Update to v1.4.3 stable release
|
||||
|
||||
-- Avenge Media <AvengeMedia.US@gmail.com> Tue, 25 Feb 2026 02:40:00 +0000
|
||||
@@ -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
|
||||
Recommends: niri | hyprland | sway
|
||||
Description: DankMaterialShell greeter for greetd
|
||||
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
||||
inspired greeter interface built with Quickshell for Wayland compositors.
|
||||
.
|
||||
Supports multiple compositors including Niri, Hyprland, and Sway with automatic
|
||||
compositor detection and configuration. Features session selection, user
|
||||
authentication, and dynamic theming.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -28,7 +28,6 @@ Depends: ${misc:Depends},
|
||||
qml6-module-qtquick-templates,
|
||||
qml6-module-qtquick-window,
|
||||
qt6ct
|
||||
Suggests: cups-pk-helper
|
||||
Conflicts: dms-git
|
||||
Replaces: dms-git
|
||||
Description: DankMaterialShell - Modern Wayland Desktop Shell
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
%global debug_package %{nil}
|
||||
%global version {{{ git_repo_version }}}
|
||||
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
||||
%global go_toolchain_version 1.25.7
|
||||
|
||||
Name: dms
|
||||
Epoch: 2
|
||||
@@ -15,12 +14,12 @@ License: MIT
|
||||
URL: https://github.com/AvengeMedia/DankMaterialShell
|
||||
VCS: {{{ git_repo_vcs }}}
|
||||
Source0: {{{ git_repo_pack }}}
|
||||
Source1: https://go.dev/dl/go%{go_toolchain_version}.linux-amd64.tar.gz
|
||||
Source2: https://go.dev/dl/go%{go_toolchain_version}.linux-arm64.tar.gz
|
||||
|
||||
BuildRequires: git-core
|
||||
BuildRequires: gzip
|
||||
BuildRequires: golang >= 1.24
|
||||
BuildRequires: make
|
||||
BuildRequires: wget
|
||||
BuildRequires: systemd-rpm-macros
|
||||
|
||||
# Core requirements
|
||||
@@ -29,7 +28,7 @@ Requires: accountsservice
|
||||
Requires: dms-cli = %{epoch}:%{version}-%{release}
|
||||
Requires: dgop
|
||||
|
||||
# Core utilities (Recommended for DMS functionality)
|
||||
# Core utilities (Highly recommended for DMS functionality)
|
||||
Recommends: cava
|
||||
Recommends: danksearch
|
||||
Recommends: matugen
|
||||
@@ -38,7 +37,6 @@ Recommends: quickshell-git
|
||||
# Recommended system packages
|
||||
Recommends: NetworkManager
|
||||
Recommends: qt6-qtmultimedia
|
||||
Suggests: cups-pk-helper
|
||||
Suggests: qt6ct
|
||||
|
||||
%description
|
||||
@@ -67,28 +65,6 @@ Provides native DBus bindings, NetworkManager integration, and system utilities.
|
||||
VERSION="%{version}"
|
||||
COMMIT=$(echo "%{version}" | grep -oP '[a-f0-9]{7,}' | head -n1 || echo "unknown")
|
||||
|
||||
# Use pinned bundled Go toolchain (deterministic across chroots)
|
||||
case "%{_arch}" in
|
||||
x86_64)
|
||||
GO_TARBALL="%{_sourcedir}/go%{go_toolchain_version}.linux-amd64.tar.gz"
|
||||
;;
|
||||
aarch64)
|
||||
GO_TARBALL="%{_sourcedir}/go%{go_toolchain_version}.linux-arm64.tar.gz"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported architecture for bundled Go: %{_arch}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
rm -rf .go
|
||||
tar -xzf "$GO_TARBALL"
|
||||
mv go .go
|
||||
export GOROOT="$PWD/.go"
|
||||
export PATH="$GOROOT/bin:$PATH"
|
||||
export GOTOOLCHAIN=local
|
||||
go version
|
||||
|
||||
cd core
|
||||
make dist VERSION="$VERSION" COMMIT="$COMMIT"
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ Recommends: danksearch
|
||||
Recommends: matugen
|
||||
Recommends: NetworkManager
|
||||
Recommends: qt6-qtmultimedia
|
||||
Suggests: cups-pk-helper
|
||||
Suggests: qt6ct
|
||||
|
||||
%description
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
dmsPkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
@@ -9,7 +10,7 @@ let
|
||||
in
|
||||
{
|
||||
packages = [
|
||||
cfg.package
|
||||
dmsPkgs.dms-shell
|
||||
]
|
||||
++ lib.optional cfg.enableSystemMonitoring cfg.dgop.package
|
||||
++ lib.optionals cfg.enableVPN [
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
let
|
||||
inherit (lib) types;
|
||||
cfg = config.programs.dank-material-shell.greeter;
|
||||
cfgDms = config.programs.dank-material-shell;
|
||||
|
||||
inherit (config.services.greetd.settings.default_session) user;
|
||||
|
||||
@@ -30,13 +29,13 @@ let
|
||||
lib.escapeShellArgs (
|
||||
[
|
||||
"sh"
|
||||
"${cfg.package}/share/quickshell/dms/Modules/Greetd/assets/dms-greeter"
|
||||
"${../../quickshell/Modules/Greetd/assets/dms-greeter}"
|
||||
"--cache-dir"
|
||||
cacheDir
|
||||
"--command"
|
||||
cfg.compositor.name
|
||||
"-p"
|
||||
"${cfg.package}/share/quickshell/dms"
|
||||
"${dmsPkgs.dms-shell}/share/quickshell/dms"
|
||||
]
|
||||
++ lib.optionals (cfg.compositor.customConfig != "") [
|
||||
"-C"
|
||||
@@ -66,21 +65,6 @@ in
|
||||
|
||||
options.programs.dank-material-shell.greeter = {
|
||||
enable = lib.mkEnableOption "DankMaterialShell greeter";
|
||||
package = lib.mkOption {
|
||||
type = types.package;
|
||||
default = if cfgDms.enable or false then cfgDms.package else dmsPkgs.dms-shell;
|
||||
defaultText = lib.literalExpression ''
|
||||
if config.programs.dank-material-shell.enable
|
||||
then config.programs.dank-material-shell.package
|
||||
else built from source;
|
||||
'';
|
||||
description = ''
|
||||
The DankMaterialShell package to use for the greeter.
|
||||
|
||||
Defaults to the package from `programs.dank-material-shell` if it is enabled,
|
||||
otherwise defaults to building from source.
|
||||
'';
|
||||
};
|
||||
compositor.name = lib.mkOption {
|
||||
type = types.enum [
|
||||
"niri"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
dmsPkgs,
|
||||
...
|
||||
}@args:
|
||||
let
|
||||
@@ -12,6 +13,7 @@ let
|
||||
config
|
||||
pkgs
|
||||
lib
|
||||
dmsPkgs
|
||||
;
|
||||
};
|
||||
hasPluginSettings = lib.any (plugin: plugin.settings != { }) (
|
||||
@@ -94,7 +96,7 @@ in
|
||||
};
|
||||
|
||||
Service = {
|
||||
ExecStart = lib.getExe cfg.package + " run --session";
|
||||
ExecStart = lib.getExe dmsPkgs.dms-shell + " run --session";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
dmsPkgs,
|
||||
...
|
||||
}@args:
|
||||
let
|
||||
@@ -11,6 +12,7 @@ let
|
||||
config
|
||||
pkgs
|
||||
lib
|
||||
dmsPkgs
|
||||
;
|
||||
};
|
||||
in
|
||||
@@ -34,7 +36,7 @@ in
|
||||
restartIfChanged = cfg.systemd.restartIfChanged;
|
||||
|
||||
serviceConfig = {
|
||||
ExecStart = lib.getExe cfg.package + " run --session";
|
||||
ExecStart = lib.getExe dmsPkgs.dms-shell + " run --session";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
};
|
||||
@@ -48,7 +50,6 @@ in
|
||||
|
||||
services.power-profiles-daemon.enable = lib.mkDefault true;
|
||||
services.accounts-daemon.enable = lib.mkDefault true;
|
||||
services.geoclue2.enable = lib.mkDefault true;
|
||||
security.polkit.enable = lib.mkDefault true;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,9 +26,6 @@ in
|
||||
|
||||
options.programs.dank-material-shell = {
|
||||
enable = lib.mkEnableOption "DankMaterialShell";
|
||||
package = lib.mkPackageOption dmsPkgs "dms-shell" {
|
||||
extraDescription = "The DankMaterialShell package to use (defaults to be built from source)";
|
||||
};
|
||||
|
||||
systemd = {
|
||||
enable = lib.mkEnableOption "DankMaterialShell systemd startup";
|
||||
|
||||
@@ -25,7 +25,6 @@ Recommends: matugen
|
||||
Recommends: quickshell-git
|
||||
Recommends: NetworkManager
|
||||
Recommends: qt6-qtmultimedia
|
||||
Suggests: cups-pk-helper
|
||||
Suggests: qt6ct
|
||||
|
||||
Provides: dms
|
||||
@@ -56,10 +55,8 @@ mkdir -p $HOME $GOCACHE $GOMODCACHE
|
||||
# OBS has no network access, so use local toolchain only
|
||||
export GOTOOLCHAIN=local
|
||||
|
||||
# Pin go.mod and vendor/modules.txt to the installed Go toolchain version
|
||||
GO_INSTALLED=$(go version | grep -oP 'go\K[0-9]+\.[0-9]+')
|
||||
sed -i "s/^go [0-9]\+\.[0-9]\+\(\.[0-9]*\)\?$/go ${GO_INSTALLED}/" core/go.mod
|
||||
sed -i "s/^\(## explicit; go \)[0-9]\+\.[0-9]\+\(\.[0-9]*\)\?$/\1${GO_INSTALLED}/" core/vendor/modules.txt
|
||||
# Patch go.mod to use base Go version (e.g., go 1.24 instead of go 1.24.6)
|
||||
sed -i 's/^go 1\.24\.[0-9]*/go 1.24/' core/go.mod
|
||||
|
||||
# Extract version info for embedding in binary
|
||||
VERSION="%{version}"
|
||||
|
||||
@@ -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
|
||||
@@ -27,7 +27,6 @@ Recommends: danksearch
|
||||
Recommends: matugen
|
||||
Recommends: NetworkManager
|
||||
Recommends: qt6-qtmultimedia
|
||||
Suggests: cups-pk-helper
|
||||
Suggests: qt6ct
|
||||
|
||||
%description
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
OBS_BASE_PROJECT="home:AvengeMedia"
|
||||
OBS_BASE="$HOME/.cache/osc-checkouts"
|
||||
|
||||
ALL_PACKAGES=(dms dms-git dms-greeter)
|
||||
ALL_PACKAGES=(dms dms-git)
|
||||
|
||||
REPOS=("Debian_13" "openSUSE_Tumbleweed" "16.0")
|
||||
ARCHES=("x86_64" "aarch64")
|
||||
@@ -41,9 +41,6 @@ for pkg in "${PACKAGES[@]}"; do
|
||||
dms-git)
|
||||
PROJECT="$OBS_BASE_PROJECT:dms-git"
|
||||
;;
|
||||
dms-greeter)
|
||||
PROJECT="$OBS_BASE_PROJECT:danklinux"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown package '$pkg'"
|
||||
continue
|
||||
@@ -77,15 +74,11 @@ for pkg in "${PACKAGES[@]}"; do
|
||||
COLOR="\033[0;32m" # Green
|
||||
SYMBOL="✅"
|
||||
;;
|
||||
failed|broken|broken*)
|
||||
failed)
|
||||
COLOR="\033[0;31m" # Red
|
||||
SYMBOL="❌"
|
||||
FAILED_BUILDS+=("$repo $arch")
|
||||
;;
|
||||
blocked)
|
||||
COLOR="\033[0;33m" # Yellow
|
||||
SYMBOL="⏸️"
|
||||
;;
|
||||
unresolvable)
|
||||
COLOR="\033[0;33m" # Yellow
|
||||
SYMBOL="⚠️"
|
||||
|
||||
@@ -68,14 +68,13 @@ fi
|
||||
|
||||
OBS_BASE_PROJECT="home:AvengeMedia"
|
||||
OBS_BASE="$HOME/.cache/osc-checkouts"
|
||||
AVAILABLE_PACKAGES=(dms dms-git dms-greeter)
|
||||
AVAILABLE_PACKAGES=(dms dms-git)
|
||||
|
||||
if [[ -z "$PACKAGE" ]]; then
|
||||
echo "Available packages:"
|
||||
echo ""
|
||||
echo " 1. dms - Stable DMS"
|
||||
echo " 2. dms-git - Nightly DMS"
|
||||
echo " 3. dms-greeter - DMS greeter for greetd"
|
||||
echo " a. all"
|
||||
echo ""
|
||||
read -r -p "Select package (1-${#AVAILABLE_PACKAGES[@]}, a): " selection
|
||||
@@ -142,12 +141,7 @@ check_obs_version_exists() {
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
# Empty/invalid response: expected on first upload (no spec on server yet), or actual API failure
|
||||
if [[ -z "$OBS_SPEC" ]]; then
|
||||
echo " No existing spec on OBS (first upload?) - proceeding"
|
||||
else
|
||||
echo "⚠️ Could not fetch OBS spec (API may be unavailable), proceeding anyway"
|
||||
fi
|
||||
echo "⚠️ Could not fetch OBS spec (API may be unavailable), proceeding anyway"
|
||||
return 1
|
||||
fi
|
||||
return 1
|
||||
@@ -174,22 +168,6 @@ update_debian_dms_service() {
|
||||
sed -i "s|/releases/download/v[0-9][^\"]*/dms-distropkg-arm64\.gz|/releases/download/v${base_version}/dms-distropkg-arm64.gz|" "$service_path"
|
||||
}
|
||||
|
||||
update_debian_dms_greeter_service() {
|
||||
local service_path="$1"
|
||||
if [[ -z "$service_path" || ! -f "$service_path" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ -z "$CHANGELOG_VERSION" ]]; then
|
||||
return 0
|
||||
fi
|
||||
local base_version
|
||||
base_version=$(echo "$CHANGELOG_VERSION" | sed -E 's/^([0-9]+(\.[0-9]+)*).*/\1/')
|
||||
if [[ -z "$base_version" ]]; then
|
||||
return 0
|
||||
fi
|
||||
sed -i "s|/releases/download/v[0-9][^\"]*/dms-qml\.tar\.gz|/releases/download/v${base_version}/dms-qml.tar.gz|" "$service_path"
|
||||
}
|
||||
|
||||
update_opensuse_git_spec() {
|
||||
local spec_path="$1"
|
||||
if [[ -z "$spec_path" || ! -f "$spec_path" ]]; then
|
||||
@@ -270,9 +248,6 @@ dms)
|
||||
dms-git)
|
||||
PROJECT="dms-git"
|
||||
;;
|
||||
dms-greeter)
|
||||
PROJECT="danklinux"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown package '$PACKAGE'"
|
||||
exit 1
|
||||
@@ -354,11 +329,9 @@ if [[ -d "distro/debian/$PACKAGE/debian" ]]; then
|
||||
echo " - Applied rebuild suffix: $CHANGELOG_VERSION"
|
||||
fi
|
||||
|
||||
# Keep Debian _service in sync with changelog version
|
||||
# Keep Debian dms _service in sync with changelog version
|
||||
if [[ "$PACKAGE" == "dms" ]] && [[ -f "distro/debian/$PACKAGE/_service" ]]; then
|
||||
update_debian_dms_service "distro/debian/$PACKAGE/_service"
|
||||
elif [[ "$PACKAGE" == "dms-greeter" ]] && [[ -f "distro/debian/$PACKAGE/_service" ]]; then
|
||||
update_debian_dms_greeter_service "distro/debian/$PACKAGE/_service"
|
||||
fi
|
||||
|
||||
# Check if this version already exists in OBS
|
||||
@@ -368,12 +341,6 @@ if [[ -d "distro/debian/$PACKAGE/debian" ]]; then
|
||||
if [[ "$PACKAGE" == *"-git" ]]; then
|
||||
echo "==> Error: This commit is already uploaded to OBS"
|
||||
echo " The same git commit ($(echo "$CHANGELOG_VERSION" | grep -oP '[a-f0-9]{8}' | tail -1)) already exists on OBS."
|
||||
if [[ -n "${GITHUB_ACTIONS:-}" ]] || [[ -n "${CI:-}" ]]; then
|
||||
echo " CI run detected: skipping upload as a no-op (already up to date)."
|
||||
echo " If you need to force rebuild this same commit, set REBUILD_RELEASE (e.g. 2, 3, ...)."
|
||||
echo "✓ Exiting gracefully (no changes needed)"
|
||||
exit 0
|
||||
fi
|
||||
echo " To rebuild the same commit, specify a rebuild number:"
|
||||
echo " ./distro/scripts/obs-upload.sh $PACKAGE 2"
|
||||
echo " ./distro/scripts/obs-upload.sh $PACKAGE 3"
|
||||
@@ -412,16 +379,6 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ -f "distro/opensuse/$PACKAGE.spec" ]];
|
||||
|
||||
if [[ "$PACKAGE" == *"-git" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then
|
||||
update_opensuse_git_spec "$WORK_DIR/$PACKAGE.spec"
|
||||
elif [[ "$PACKAGE" == "dms-greeter" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then
|
||||
DMS_GREETER_BASE_VERSION=$(echo "$CHANGELOG_VERSION" | sed -E 's/^([0-9]+(\.[0-9]+)*).*/\1/')
|
||||
DMS_GREETER_RELEASE=$(echo "$CHANGELOG_VERSION" | sed -E 's/.*db([0-9]+)$/\1/' || echo "1")
|
||||
CHANGELOG_DATE=$(date '+%a %b %d %Y')
|
||||
sed -i "s/VERSION_PLACEHOLDER/${DMS_GREETER_BASE_VERSION}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||
sed -i "s/RELEASE_PLACEHOLDER/${DMS_GREETER_RELEASE}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||
# Explicitly set Version:/Release: in case the spec uses %{version} macro
|
||||
sed -i "s/^Version:.*/Version: ${DMS_GREETER_BASE_VERSION}/" "$WORK_DIR/$PACKAGE.spec"
|
||||
sed -i "s/^Release:.*/Release: ${DMS_GREETER_RELEASE}%{?dist}/" "$WORK_DIR/$PACKAGE.spec"
|
||||
fi
|
||||
|
||||
if [[ -f "$WORK_DIR/.osc/$PACKAGE.spec" ]]; then
|
||||
@@ -487,24 +444,6 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ "$UPLOAD_DEBIAN" == false ]] && [[ -f
|
||||
fi
|
||||
fi
|
||||
|
||||
# For dms-greeter: download dms-qml.tar.gz from _service URL
|
||||
if [[ -z "${SOURCE_DIR:-}" ]] && [[ "$PACKAGE" == "dms-greeter" ]] && [[ -f "distro/debian/$PACKAGE/_service" ]]; then
|
||||
DMS_GREETER_URL=$(grep -A 5 'name="download_url"' "distro/debian/$PACKAGE/_service" | grep "path" | sed 's/.*<param name="path">\(.*\)<\/param>.*/\1/' | head -1)
|
||||
if [[ -n "$DMS_GREETER_URL" ]]; then
|
||||
DMS_GREETER_FULL_URL="https://github.com${DMS_GREETER_URL}"
|
||||
echo " Downloading dms-greeter source from: $DMS_GREETER_FULL_URL"
|
||||
if wget -q -O "$TEMP_DIR/dms-qml.tar.gz" "$DMS_GREETER_FULL_URL" 2>/dev/null || \
|
||||
curl -L -f -s -o "$TEMP_DIR/dms-qml.tar.gz" "$DMS_GREETER_FULL_URL" 2>/dev/null; then
|
||||
cd "$TEMP_DIR"
|
||||
tar -xzf dms-qml.tar.gz
|
||||
if [[ -f "Modules/Greetd/assets/dms-greeter" ]]; then
|
||||
SOURCE_DIR="$TEMP_DIR"
|
||||
fi
|
||||
cd "$REPO_ROOT"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$SOURCE_DIR" && -d "$SOURCE_DIR" ]]; then
|
||||
SOURCE0=$(grep "^Source0:" "distro/opensuse/$PACKAGE.spec" | awk '{print $2}' | head -1)
|
||||
|
||||
@@ -513,15 +452,6 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ "$UPLOAD_DEBIAN" == false ]] && [[ -f
|
||||
cd "$OBS_TARBALL_DIR"
|
||||
|
||||
case "$PACKAGE" in
|
||||
dms-greeter)
|
||||
EXPECTED_DIR="dms-qml"
|
||||
echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)"
|
||||
mkdir -p "$EXPECTED_DIR"
|
||||
cp -a "$SOURCE_DIR"/. "$EXPECTED_DIR/"
|
||||
tar -czf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
rm -rf "$EXPECTED_DIR"
|
||||
echo " Created $SOURCE0 ($(stat -c%s "$WORK_DIR/$SOURCE0" 2>/dev/null || echo 0) bytes)"
|
||||
;;
|
||||
dms)
|
||||
DMS_VERSION=$(grep "^Version:" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec" | sed 's/^Version:[[:space:]]*//' | head -1)
|
||||
EXPECTED_DIR="DankMaterialShell-${DMS_VERSION}"
|
||||
@@ -654,17 +584,6 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
|
||||
if [[ -z "$SOURCE_DIR" ]]; then
|
||||
SOURCE_DIR=$(find . -maxdepth 1 -type d ! -name "." | head -1)
|
||||
fi
|
||||
# dms-qml.tar.gz extracts flat (quickshell contents, no top-level dir)
|
||||
# Create dms-qml wrapper so combined tarball has correct top-level dir (like dms)
|
||||
if [[ "$PACKAGE" == "dms-greeter" ]] && [[ -f "Modules/Greetd/assets/dms-greeter" ]]; then
|
||||
mkdir -p dms-qml
|
||||
for f in *; do
|
||||
if [[ -e "$f" && "$f" != "dms-qml" && "$f" != "source-archive" ]]; then
|
||||
mv "$f" dms-qml/ 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
SOURCE_DIR="dms-qml"
|
||||
fi
|
||||
if [[ -z "$SOURCE_DIR" || ! -d "$SOURCE_DIR" ]]; then
|
||||
echo "Error: Failed to extract source archive or find source directory"
|
||||
echo "Contents of $TEMP_DIR:"
|
||||
@@ -741,21 +660,6 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
|
||||
cd "$OBS_TARBALL_DIR"
|
||||
|
||||
case "$PACKAGE" in
|
||||
dms-greeter)
|
||||
EXPECTED_DIR="dms-qml"
|
||||
echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)"
|
||||
mkdir -p "$EXPECTED_DIR"
|
||||
cp -a "$SOURCE_DIR"/. "$EXPECTED_DIR/"
|
||||
if [[ "$SOURCE0" == *.tar.xz ]]; then
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
elif [[ "$SOURCE0" == *.tar.bz2 ]]; then
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
else
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
fi
|
||||
rm -rf "$EXPECTED_DIR"
|
||||
echo " Created $SOURCE0 ($(stat -c%s "$WORK_DIR/$SOURCE0" 2>/dev/null || echo 0) bytes)"
|
||||
;;
|
||||
dms)
|
||||
DMS_VERSION=$(grep "^Version:" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec" | sed 's/^Version:[[:space:]]*//' | head -1)
|
||||
EXPECTED_DIR="DankMaterialShell-${DMS_VERSION}"
|
||||
@@ -805,20 +709,10 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
|
||||
echo " - OpenSUSE source tarballs created"
|
||||
fi
|
||||
|
||||
# Copy and update OpenSUSE spec file with the correct version
|
||||
# Copy and update OpenSUSE spec file with the correct version (for -git packages)
|
||||
cp "distro/opensuse/$PACKAGE.spec" "$WORK_DIR/"
|
||||
if [[ "$PACKAGE" == *"-git" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then
|
||||
update_opensuse_git_spec "$WORK_DIR/$PACKAGE.spec"
|
||||
elif [[ "$PACKAGE" == "dms-greeter" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then
|
||||
DMS_GREETER_BASE_VERSION=$(echo "$CHANGELOG_VERSION" | sed -E 's/^([0-9]+(\.[0-9]+)*).*/\1/')
|
||||
DMS_GREETER_RELEASE=$(echo "$CHANGELOG_VERSION" | sed -E 's/.*db([0-9]+)$/\1/' || echo "1")
|
||||
CHANGELOG_DATE=$(date '+%a %b %d %Y')
|
||||
sed -i "s/VERSION_PLACEHOLDER/${DMS_GREETER_BASE_VERSION}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||
sed -i "s/RELEASE_PLACEHOLDER/${DMS_GREETER_RELEASE}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||
# Explicitly set Version:/Release: in case the spec uses %{version} macro
|
||||
sed -i "s/^Version:.*/Version: ${DMS_GREETER_BASE_VERSION}/" "$WORK_DIR/$PACKAGE.spec"
|
||||
sed -i "s/^Release:.*/Release: ${DMS_GREETER_RELEASE}%{?dist}/" "$WORK_DIR/$PACKAGE.spec"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -945,48 +839,8 @@ EOF
|
||||
echo " - Quilt format detected: creating debian.tar.gz"
|
||||
tar -czf "$WORK_DIR/debian.tar.gz" -C "distro/debian/$PACKAGE" debian/
|
||||
|
||||
# For dms-greeter: create orig tarball so Debian build gets upstream (OBS only passes .dsc Files to Debian)
|
||||
DSC_FILES_DEBIAN=""
|
||||
if [[ "$PACKAGE" == "dms-greeter" ]]; then
|
||||
UPSTREAM_VER=$(echo "$VERSION" | sed 's/-[^-]*$//')
|
||||
ORIG_TARBALL="${PACKAGE}_${UPSTREAM_VER}.orig.tar.gz"
|
||||
ORIG_DIR="${PACKAGE}-${UPSTREAM_VER}"
|
||||
|
||||
if [[ -f "distro/debian/$PACKAGE/_service" ]] && grep -q "download_url" "distro/debian/$PACKAGE/_service"; then
|
||||
DG_TEMP=$(mktemp -d)
|
||||
DMS_GREETER_PATH=$(grep -A 5 'name="download_url"' "distro/debian/$PACKAGE/_service" | grep "path" | sed 's/.*<param name="path">\(.*\)<\/param>.*/\1/' | head -1)
|
||||
if [[ -n "$DMS_GREETER_PATH" ]]; then
|
||||
DG_URL="https://github.com${DMS_GREETER_PATH}"
|
||||
echo " - Downloading dms-greeter source for orig tarball: $DG_URL"
|
||||
if wget -q -O "$DG_TEMP/dms-qml.tar.gz" "$DG_URL" 2>/dev/null || curl -L -f -s -o "$DG_TEMP/dms-qml.tar.gz" "$DG_URL" 2>/dev/null; then
|
||||
( cd "$DG_TEMP" && tar --no-same-owner -xzf dms-qml.tar.gz && mkdir -p "$ORIG_DIR" && \
|
||||
for f in *; do [[ "$f" != "dms-qml.tar.gz" && "$f" != "$ORIG_DIR" ]] && mv "$f" "$ORIG_DIR/"; done )
|
||||
if [[ -d "$DG_TEMP/$ORIG_DIR/Modules" ]] || [[ -f "$DG_TEMP/$ORIG_DIR/LICENSE" ]]; then
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$ORIG_TARBALL" -C "$DG_TEMP" "$ORIG_DIR"
|
||||
ORIG_MD5=$(md5sum "$WORK_DIR/$ORIG_TARBALL" | cut -d' ' -f1)
|
||||
ORIG_SIZE=$(stat -c%s "$WORK_DIR/$ORIG_TARBALL" 2>/dev/null || stat -f%z "$WORK_DIR/$ORIG_TARBALL" 2>/dev/null)
|
||||
DSC_FILES_DEBIAN=" $ORIG_MD5 $ORIG_SIZE $ORIG_TARBALL
|
||||
"
|
||||
echo " - Created $ORIG_TARBALL for Debian orig"
|
||||
fi
|
||||
rm -rf "$DG_TEMP"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
DEBIAN_MD5=$(md5sum "$WORK_DIR/debian.tar.gz" | cut -d' ' -f1)
|
||||
DEBIAN_SIZE=$(stat -c%s "$WORK_DIR/debian.tar.gz" 2>/dev/null || stat -f%z "$WORK_DIR/debian.tar.gz" 2>/dev/null)
|
||||
|
||||
echo " - Generating $PACKAGE.dsc for quilt format"
|
||||
# debtransform: DEBTRANSFORM-TAR = orig (upstream), DEBTRANSFORM-FILES-TAR = debian archive
|
||||
DEBTRANSFORM_EXTRA=""
|
||||
if [[ -n "$DSC_FILES_DEBIAN" ]] && [[ -n "$ORIG_TARBALL" ]]; then
|
||||
DEBTRANSFORM_EXTRA="DEBTRANSFORM-TAR: $ORIG_TARBALL
|
||||
DEBTRANSFORM-FILES-TAR: debian.tar.gz
|
||||
"
|
||||
fi
|
||||
cat >"$WORK_DIR/$PACKAGE.dsc" <<DSCEOF
|
||||
cat >"$WORK_DIR/$PACKAGE.dsc" <<EOF
|
||||
Format: 3.0 (quilt)
|
||||
Source: $PACKAGE
|
||||
Binary: $PACKAGE
|
||||
@@ -994,9 +848,10 @@ Architecture: any
|
||||
Version: $VERSION
|
||||
Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
|
||||
Build-Depends: debhelper-compat (= 13), wget, gzip
|
||||
${DEBTRANSFORM_EXTRA}Files:${DSC_FILES_DEBIAN}
|
||||
$DEBIAN_MD5 $DEBIAN_SIZE debian.tar.gz
|
||||
DSCEOF
|
||||
DEBTRANSFORM-TAR: debian.tar.gz
|
||||
Files:
|
||||
00000000000000000000000000000000 1 debian.tar.gz
|
||||
EOF
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
@@ -1027,13 +882,6 @@ if [[ -n "$OBS_FILES" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Keep current orig tarball for dms-greeter (Debian 3.0 quilt needs it)
|
||||
UPSTREAM_VER_CLEAN=$(echo "$CHANGELOG_VERSION" | sed 's/-[^-]*$//' 2>/dev/null)
|
||||
if [[ "$PACKAGE" == "dms-greeter" ]] && [[ "$old_file" == "${PACKAGE}_${UPSTREAM_VER_CLEAN}.orig.tar.gz" ]]; then
|
||||
echo " - Keeping orig tarball: $old_file"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$old_file" == "${PACKAGE}-source.tar.gz" ]]; then
|
||||
echo " - Keeping source tarball: $old_file"
|
||||
continue
|
||||
|
||||
@@ -28,17 +28,8 @@ override_dh_auto_build:
|
||||
# Launchpad build environment has no internet access
|
||||
test -d dms-git-repo || (echo "ERROR: dms-git-repo directory not found!" && exit 1)
|
||||
|
||||
# Patch go.mod for Launchpad: align go directive w/latest Go toolchain
|
||||
GO_VERSION=$$(go env GOVERSION | sed -E 's/^go([0-9]+\.[0-9]+).*/\1/'); \
|
||||
if [ -n "$$GO_VERSION" ]; then \
|
||||
sed -E -i "s/^go 1\.[0-9]+(\.[0-9]+)?/go $$GO_VERSION/" dms-git-repo/core/go.mod; \
|
||||
if [ -f dms-git-repo/core/vendor/modules.txt ]; then \
|
||||
sed -E -i "s/^(## explicit; go )1\.[0-9]+(\.[0-9]+)?$$/\1$$GO_VERSION/" dms-git-repo/core/vendor/modules.txt; \
|
||||
fi; \
|
||||
else \
|
||||
echo "Warning: Could not detect Go version, leaving go.mod go directive unchanged"; \
|
||||
fi
|
||||
sed -E -i '/^toolchain go[0-9.]+$$/d' dms-git-repo/core/go.mod
|
||||
# Patch go.mod to use Go 1.24 base version (Ubuntu has 1.24.4, project requires 1.24.6)
|
||||
sed -i 's/^go 1\.24\.[0-9]*/go 1.24/' dms-git-repo/core/go.mod
|
||||
|
||||
# Build dms-cli from source
|
||||
# Detect architecture
|
||||
|
||||
6
flake.lock
generated
6
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1771369470,
|
||||
"narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=",
|
||||
"lastModified": 1769018530,
|
||||
"narHash": "sha256-MJ27Cy2NtBEV5tsK+YraYr2g851f3Fl1LpNHDzDX15c=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "0182a361324364ae3f436a63005877674cf45efb",
|
||||
"rev": "88d3861acdd3d2f0e361767018218e51810df8a1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
119
flake.nix
119
flake.nix
@@ -72,82 +72,76 @@
|
||||
"${cleanVersion}${dateSuffix}${revSuffix}";
|
||||
in
|
||||
{
|
||||
dms-shell = pkgs.lib.makeOverridable (
|
||||
dms-shell = pkgs.buildGoModule (
|
||||
let
|
||||
rootSrc = ./.;
|
||||
in
|
||||
{
|
||||
extraQtPackages ? [ ],
|
||||
}:
|
||||
pkgs.buildGoModule (
|
||||
let
|
||||
rootSrc = ./.;
|
||||
qtPackages = (qmlPkgs pkgs) ++ extraQtPackages;
|
||||
in
|
||||
{
|
||||
inherit version;
|
||||
pname = "dms-shell";
|
||||
src = ./core;
|
||||
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ=";
|
||||
inherit version;
|
||||
pname = "dms-shell";
|
||||
src = ./core;
|
||||
vendorHash = "sha256-cVUJXgzYMRSM0od1xzDVkMTdxHu3OIQX2bQ8AJbGQ1Q=";
|
||||
|
||||
subPackages = [ "cmd/dms" ];
|
||||
subPackages = [ "cmd/dms" ];
|
||||
|
||||
ldflags = [
|
||||
"-s"
|
||||
"-w"
|
||||
"-X 'main.Version=${version}'"
|
||||
];
|
||||
ldflags = [
|
||||
"-s"
|
||||
"-w"
|
||||
"-X 'main.Version=${version}'"
|
||||
];
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
installShellFiles
|
||||
makeWrapper
|
||||
];
|
||||
nativeBuildInputs = with pkgs; [
|
||||
installShellFiles
|
||||
makeWrapper
|
||||
];
|
||||
|
||||
postInstall = ''
|
||||
mkdir -p $out/share/quickshell/dms
|
||||
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
|
||||
postInstall = ''
|
||||
mkdir -p $out/share/quickshell/dms
|
||||
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
|
||||
|
||||
chmod u+w $out/share/quickshell/dms/VERSION
|
||||
echo "${version}" > $out/share/quickshell/dms/VERSION
|
||||
chmod u+w $out/share/quickshell/dms/VERSION
|
||||
echo "${version}" > $out/share/quickshell/dms/VERSION
|
||||
|
||||
# Install desktop file and icon
|
||||
install -D ${rootSrc}/assets/dms-open.desktop \
|
||||
$out/share/applications/dms-open.desktop
|
||||
install -D ${rootSrc}/core/assets/danklogo.svg \
|
||||
$out/share/hicolor/scalable/apps/danklogo.svg
|
||||
# Install desktop file and icon
|
||||
install -D ${rootSrc}/assets/dms-open.desktop \
|
||||
$out/share/applications/dms-open.desktop
|
||||
install -D ${rootSrc}/core/assets/danklogo.svg \
|
||||
$out/share/hicolor/scalable/apps/danklogo.svg
|
||||
|
||||
wrapProgram $out/bin/dms \
|
||||
--add-flags "-c $out/share/quickshell/dms" \
|
||||
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs qtPackages}" \
|
||||
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs qtPackages}"
|
||||
wrapProgram $out/bin/dms \
|
||||
--add-flags "-c $out/share/quickshell/dms" \
|
||||
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs (qmlPkgs pkgs)}" \
|
||||
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs (qmlPkgs pkgs)}"
|
||||
|
||||
install -Dm644 ${rootSrc}/assets/systemd/dms.service \
|
||||
$out/lib/systemd/user/dms.service
|
||||
install -Dm644 ${rootSrc}/assets/systemd/dms.service \
|
||||
$out/lib/systemd/user/dms.service
|
||||
|
||||
substituteInPlace $out/lib/systemd/user/dms.service \
|
||||
--replace-fail /usr/bin/dms $out/bin/dms \
|
||||
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
|
||||
substituteInPlace $out/lib/systemd/user/dms.service \
|
||||
--replace-fail /usr/bin/dms $out/bin/dms \
|
||||
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
|
||||
|
||||
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
|
||||
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
|
||||
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
|
||||
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
|
||||
|
||||
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
|
||||
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
|
||||
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
|
||||
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
|
||||
|
||||
installShellCompletion --cmd dms \
|
||||
--bash <($out/bin/dms completion bash) \
|
||||
--fish <($out/bin/dms completion fish) \
|
||||
--zsh <($out/bin/dms completion zsh)
|
||||
'';
|
||||
installShellCompletion --cmd dms \
|
||||
--bash <($out/bin/dms completion bash) \
|
||||
--fish <($out/bin/dms completion fish) \
|
||||
--zsh <($out/bin/dms completion zsh)
|
||||
'';
|
||||
|
||||
meta = {
|
||||
description = "Desktop shell for wayland compositors built with Quickshell & GO";
|
||||
homepage = "https://danklinux.com";
|
||||
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
|
||||
license = pkgs.lib.licenses.mit;
|
||||
mainProgram = "dms";
|
||||
platforms = pkgs.lib.platforms.linux;
|
||||
};
|
||||
}
|
||||
)
|
||||
) { };
|
||||
meta = {
|
||||
description = "Desktop shell for wayland compositors built with Quickshell & GO";
|
||||
homepage = "https://danklinux.com";
|
||||
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
|
||||
license = pkgs.lib.licenses.mit;
|
||||
mainProgram = "dms";
|
||||
platforms = pkgs.lib.platforms.linux;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
quickshell = quickshell.packages.${system}.default;
|
||||
|
||||
@@ -188,7 +182,6 @@
|
||||
with pkgs;
|
||||
[
|
||||
go_1_25
|
||||
go-mockery_2
|
||||
gopls
|
||||
delve
|
||||
go-tools
|
||||
|
||||
@@ -1 +1 @@
|
||||
The Wolverine
|
||||
Saffron Bloom
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var level: Theme.elevationLevel2
|
||||
property string direction: Theme.elevationLightDirection
|
||||
property real fallbackOffset: 4
|
||||
|
||||
property color targetColor: "white"
|
||||
property real targetRadius: Theme.cornerRadius
|
||||
property color borderColor: "transparent"
|
||||
property real borderWidth: 0
|
||||
|
||||
property bool shadowEnabled: Theme.elevationEnabled
|
||||
property real shadowBlurPx: level && level.blurPx !== undefined ? level.blurPx : 0
|
||||
property real shadowSpreadPx: level && level.spreadPx !== undefined ? level.spreadPx : 0
|
||||
property real shadowOffsetX: Theme.elevationOffsetXFor(level, direction, fallbackOffset)
|
||||
property real shadowOffsetY: Theme.elevationOffsetYFor(level, direction, fallbackOffset)
|
||||
property color shadowColor: Theme.elevationShadowColor(level)
|
||||
property real shadowOpacity: 1
|
||||
property real blurMax: Theme.elevationBlurMax
|
||||
|
||||
property alias sourceRect: sourceRect
|
||||
|
||||
layer.enabled: shadowEnabled
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
autoPaddingEnabled: true
|
||||
shadowEnabled: true
|
||||
blurEnabled: false
|
||||
maskEnabled: false
|
||||
shadowBlur: Math.max(0, Math.min(1, root.shadowBlurPx / Math.max(1, root.blurMax)))
|
||||
shadowScale: 1 + (2 * root.shadowSpreadPx) / Math.max(1, Math.min(root.width, root.height))
|
||||
shadowHorizontalOffset: root.shadowOffsetX
|
||||
shadowVerticalOffset: root.shadowOffsetY
|
||||
blurMax: root.blurMax
|
||||
shadowColor: root.shadowColor
|
||||
shadowOpacity: root.shadowOpacity
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: sourceRect
|
||||
anchors.fill: parent
|
||||
radius: root.targetRadius
|
||||
color: root.targetColor
|
||||
border.color: root.borderColor
|
||||
border.width: root.borderWidth
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Qt.labs.folderlistmodel
|
||||
import Quickshell
|
||||
@@ -9,9 +8,7 @@ import Quickshell.Io
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property string _resolvedLocale: "en"
|
||||
|
||||
readonly property string _rawLocale: SessionData.locale === "" ? Qt.locale().name : SessionData.locale
|
||||
readonly property string _rawLocale: Qt.locale().name
|
||||
readonly property string _lang: _rawLocale.split(/[_-]/)[0]
|
||||
readonly property var _candidates: {
|
||||
const fullUnderscore = _rawLocale;
|
||||
@@ -24,10 +21,7 @@ Singleton {
|
||||
|
||||
readonly property url translationsFolder: Qt.resolvedUrl("../translations/poexports")
|
||||
|
||||
readonly property alias folder: dir.folder
|
||||
property var presentLocales: ({
|
||||
"en": Qt.locale("en")
|
||||
})
|
||||
property string currentLocale: "en"
|
||||
property var translations: ({})
|
||||
property bool translationsLoaded: false
|
||||
|
||||
@@ -40,10 +34,8 @@ Singleton {
|
||||
showDirs: false
|
||||
showDotAndDotDot: false
|
||||
|
||||
onStatusChanged: if (status === FolderListModel.Ready) {
|
||||
root._loadPresentLocales();
|
||||
root._pickTranslation();
|
||||
}
|
||||
onStatusChanged: if (status === FolderListModel.Ready)
|
||||
root._pickTranslation()
|
||||
}
|
||||
|
||||
FileView {
|
||||
@@ -54,54 +46,41 @@ Singleton {
|
||||
try {
|
||||
root.translations = JSON.parse(text());
|
||||
root.translationsLoaded = true;
|
||||
console.info(`I18n: Loaded translations for '${root._resolvedLocale}' (${Object.keys(root.translations).length} contexts)`);
|
||||
console.info(`I18n: Loaded translations for '${root.currentLocale}' ` + `(${Object.keys(root.translations).length} contexts)`);
|
||||
} catch (e) {
|
||||
console.warn(`I18n: Error parsing '${root._resolvedLocale}':`, e, "- falling back to English");
|
||||
console.warn(`I18n: Error parsing '${root.currentLocale}':`, e, "- falling back to English");
|
||||
root._fallbackToEnglish();
|
||||
}
|
||||
}
|
||||
|
||||
onLoadFailed: error => {
|
||||
console.warn(`I18n: Failed to load '${root._resolvedLocale}' (${error}), ` + "falling back to English");
|
||||
console.warn(`I18n: Failed to load '${root.currentLocale}' (${error}), ` + "falling back to English");
|
||||
root._fallbackToEnglish();
|
||||
}
|
||||
}
|
||||
|
||||
function locale() {
|
||||
if (SessionData.timeLocale)
|
||||
return Qt.locale(SessionData.timeLocale);
|
||||
return Qt.locale();
|
||||
}
|
||||
|
||||
function _loadPresentLocales() {
|
||||
if (Object.keys(presentLocales).length > 1) {
|
||||
return; // already loaded
|
||||
}
|
||||
function _pickTranslation() {
|
||||
const present = new Set();
|
||||
for (let i = 0; i < dir.count; i++) {
|
||||
const name = dir.get(i, "fileName"); // e.g. "zh_CN.json"
|
||||
if (name && name.endsWith(".json")) {
|
||||
const shortName = name.slice(0, -5);
|
||||
presentLocales[shortName] = Qt.locale(shortName);
|
||||
present.add(name.slice(0, -5));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _pickTranslation() {
|
||||
for (let i = 0; i < _candidates.length; i++) {
|
||||
const cand = _candidates[i];
|
||||
if (presentLocales[cand] === undefined)
|
||||
continue;
|
||||
_resolvedLocale = cand;
|
||||
useLocale(cand, cand.startsWith("en") ? "" : translationsFolder + "/" + cand + ".json");
|
||||
return;
|
||||
if (present.has(cand)) {
|
||||
_useLocale(cand, dir.folder + "/" + cand + ".json");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_resolvedLocale = "en";
|
||||
_fallbackToEnglish();
|
||||
}
|
||||
|
||||
function useLocale(localeTag, fileUrl) {
|
||||
_resolvedLocale = localeTag || "en";
|
||||
function _useLocale(localeTag, fileUrl) {
|
||||
currentLocale = localeTag;
|
||||
_selectedPath = fileUrl;
|
||||
translationsLoaded = false;
|
||||
translations = ({});
|
||||
@@ -109,6 +88,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function _fallbackToEnglish() {
|
||||
currentLocale = "en";
|
||||
_selectedPath = "";
|
||||
translationsLoaded = false;
|
||||
translations = ({});
|
||||
|
||||
@@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import QtCore
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -71,50 +70,20 @@ Singleton {
|
||||
return appId;
|
||||
}
|
||||
|
||||
function resolveIconPath(iconName: string): string {
|
||||
if (!iconName) return "";
|
||||
const moddedId = moddedAppId(iconName);
|
||||
if (moddedId !== iconName) {
|
||||
if (moddedId.startsWith("~") || moddedId.startsWith("/"))
|
||||
return toFileUrl(expandTilde(moddedId));
|
||||
if (moddedId.startsWith("file://"))
|
||||
return moddedId;
|
||||
return Quickshell.iconPath(moddedId, true);
|
||||
}
|
||||
return Quickshell.iconPath(iconName, true) || DesktopService.resolveIconPath(iconName);
|
||||
}
|
||||
|
||||
function resolveIconUrl(iconName: string): string {
|
||||
if (!iconName) return "";
|
||||
const moddedId = moddedAppId(iconName);
|
||||
if (moddedId !== iconName) {
|
||||
if (moddedId.startsWith("~") || moddedId.startsWith("/"))
|
||||
return toFileUrl(expandTilde(moddedId));
|
||||
if (moddedId.startsWith("file://"))
|
||||
return moddedId;
|
||||
return "image://icon/" + moddedId;
|
||||
}
|
||||
return "image://icon/" + iconName;
|
||||
}
|
||||
|
||||
function getAppIcon(appId: string, desktopEntry: var): string {
|
||||
if (appId === "org.quickshell") {
|
||||
return Qt.resolvedUrl("../assets/danklogo.svg");
|
||||
}
|
||||
|
||||
const moddedId = moddedAppId(appId);
|
||||
if (moddedId !== appId)
|
||||
return resolveIconPath(appId);
|
||||
if (moddedId !== appId) {
|
||||
return Quickshell.iconPath(moddedId, true);
|
||||
}
|
||||
|
||||
if (desktopEntry && desktopEntry.icon) {
|
||||
return Quickshell.iconPath(desktopEntry.icon, true);
|
||||
}
|
||||
|
||||
const icon = Quickshell.iconPath(appId, true);
|
||||
if (icon && icon !== "")
|
||||
return icon;
|
||||
|
||||
return DesktopService.resolveIconPath(appId);
|
||||
return Quickshell.iconPath(appId, true);
|
||||
}
|
||||
|
||||
function getAppName(appId: string, desktopEntry: var): string {
|
||||
|
||||
@@ -21,9 +21,7 @@ Singleton {
|
||||
property bool _isReadOnly: false
|
||||
property bool _hasUnsavedChanges: false
|
||||
property var _loadedSessionSnapshot: null
|
||||
readonly property var _hooks: ({
|
||||
"updateLocale": updateLocale
|
||||
})
|
||||
readonly property var _hooks: ({})
|
||||
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
||||
readonly property string _stateDir: Paths.strip(_stateUrl)
|
||||
|
||||
@@ -128,13 +126,6 @@ Singleton {
|
||||
property var hiddenOutputDeviceNames: []
|
||||
property var hiddenInputDeviceNames: []
|
||||
|
||||
property string locale: ""
|
||||
property string timeLocale: ""
|
||||
|
||||
property string launcherLastMode: "all"
|
||||
property string appDrawerLastMode: "apps"
|
||||
property string niriOverviewLastMode: "apps"
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!isGreeterMode) {
|
||||
loadSettings();
|
||||
@@ -1109,29 +1100,6 @@ Singleton {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function updateLocale() {
|
||||
if (!locale) {
|
||||
I18n._pickTranslation();
|
||||
return;
|
||||
}
|
||||
I18n.useLocale(locale, locale.startsWith("en") ? "" : I18n.folder + "/" + locale + ".json");
|
||||
}
|
||||
|
||||
function setLauncherLastMode(mode) {
|
||||
launcherLastMode = mode;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setAppDrawerLastMode(mode) {
|
||||
appDrawerLastMode = mode;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setNiriOverviewLastMode(mode) {
|
||||
niriOverviewLastMode = mode;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function syncWallpaperForCurrentMode() {
|
||||
if (!perModeWallpaper)
|
||||
return;
|
||||
@@ -1245,7 +1213,7 @@ Singleton {
|
||||
id: greeterSessionFile
|
||||
|
||||
path: {
|
||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
|
||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms";
|
||||
return greetCfgDir + "/session.json";
|
||||
}
|
||||
preload: isGreeterMode
|
||||
|
||||
@@ -14,7 +14,7 @@ import "settings/SettingsStore.js" as Store
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property int settingsConfigVersion: 6
|
||||
readonly property int settingsConfigVersion: 5
|
||||
|
||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
||||
|
||||
@@ -149,7 +149,6 @@ Singleton {
|
||||
property int mangoLayoutRadiusOverride: -1
|
||||
property int mangoLayoutBorderSize: -1
|
||||
|
||||
property int firstDayOfWeek: -1
|
||||
property bool use24HourClock: true
|
||||
property bool showSeconds: false
|
||||
property bool padHours12Hour: false
|
||||
@@ -166,24 +165,6 @@ Singleton {
|
||||
property int modalCustomAnimationDuration: 150
|
||||
property bool enableRippleEffects: true
|
||||
onEnableRippleEffectsChanged: saveSettings()
|
||||
property bool m3ElevationEnabled: true
|
||||
onM3ElevationEnabledChanged: saveSettings()
|
||||
property int m3ElevationIntensity: 12
|
||||
onM3ElevationIntensityChanged: saveSettings()
|
||||
property int m3ElevationOpacity: 30
|
||||
onM3ElevationOpacityChanged: saveSettings()
|
||||
property string m3ElevationColorMode: "default"
|
||||
onM3ElevationColorModeChanged: saveSettings()
|
||||
property string m3ElevationLightDirection: "top"
|
||||
onM3ElevationLightDirectionChanged: saveSettings()
|
||||
property string m3ElevationCustomColor: "#000000"
|
||||
onM3ElevationCustomColorChanged: saveSettings()
|
||||
property bool modalElevationEnabled: true
|
||||
onModalElevationEnabledChanged: saveSettings()
|
||||
property bool popoutElevationEnabled: true
|
||||
onPopoutElevationEnabledChanged: saveSettings()
|
||||
property bool barElevationEnabled: true
|
||||
onBarElevationEnabledChanged: saveSettings()
|
||||
property string wallpaperFillMode: "Fill"
|
||||
property bool blurredWallpaperLayer: false
|
||||
property bool blurWallpaperOnOverview: false
|
||||
@@ -313,17 +294,6 @@ Singleton {
|
||||
property string centeringMode: "index"
|
||||
property string clockDateFormat: ""
|
||||
property string lockDateFormat: ""
|
||||
property bool greeterRememberLastSession: true
|
||||
property bool greeterRememberLastUser: true
|
||||
property bool greeterEnableFprint: false
|
||||
property bool greeterEnableU2f: false
|
||||
property string greeterWallpaperPath: ""
|
||||
property bool greeterUse24HourClock: true
|
||||
property bool greeterShowSeconds: false
|
||||
property bool greeterPadHours12Hour: false
|
||||
property string greeterLockDateFormat: ""
|
||||
property string greeterFontFamily: ""
|
||||
property string greeterWallpaperFillMode: ""
|
||||
property int mediaSize: 1
|
||||
|
||||
property string appLauncherViewMode: "list"
|
||||
@@ -524,15 +494,9 @@ Singleton {
|
||||
property bool enableFprint: false
|
||||
property int maxFprintTries: 15
|
||||
property bool fprintdAvailable: false
|
||||
property bool enableU2f: false
|
||||
property string u2fMode: "or"
|
||||
property bool u2fAvailable: false
|
||||
property string lockScreenActiveMonitor: "all"
|
||||
property string lockScreenInactiveColor: "#000000"
|
||||
property int lockScreenNotificationMode: 0
|
||||
property bool lockScreenVideoEnabled: false
|
||||
property string lockScreenVideoPath: ""
|
||||
property bool lockScreenVideoCycling: false
|
||||
property bool hideBrightnessSlider: false
|
||||
|
||||
property int notificationTimeoutLow: 5000
|
||||
@@ -554,7 +518,7 @@ Singleton {
|
||||
property int osdPosition: SettingsData.Position.BottomCenter
|
||||
property bool osdVolumeEnabled: true
|
||||
property bool osdMediaVolumeEnabled: true
|
||||
property bool osdMediaPlaybackEnabled: false
|
||||
property bool osdMediaPlaybackEnabled: true
|
||||
property bool osdBrightnessEnabled: true
|
||||
property bool osdIdleInhibitorEnabled: true
|
||||
property bool osdMicMuteEnabled: true
|
||||
@@ -608,10 +572,6 @@ Singleton {
|
||||
"widgetTransparency": 1.0,
|
||||
"squareCorners": false,
|
||||
"noBackground": false,
|
||||
"maximizeWidgetIcons": false,
|
||||
"maximizeWidgetText": false,
|
||||
"removeWidgetPadding": false,
|
||||
"widgetPadding": 8,
|
||||
"gothCornersEnabled": false,
|
||||
"gothCornerRadiusOverride": false,
|
||||
"gothCornerRadiusValue": 12,
|
||||
@@ -624,7 +584,6 @@ Singleton {
|
||||
"widgetOutlineOpacity": 1.0,
|
||||
"widgetOutlineThickness": 1,
|
||||
"fontScale": 1.0,
|
||||
"iconScale": 1.0,
|
||||
"autoHide": false,
|
||||
"autoHideDelay": 250,
|
||||
"showOnWindowsOpen": false,
|
||||
@@ -638,7 +597,7 @@ Singleton {
|
||||
"scrollYBehavior": "workspace",
|
||||
"shadowIntensity": 0,
|
||||
"shadowOpacity": 60,
|
||||
"shadowColorMode": "default",
|
||||
"shadowColorMode": "text",
|
||||
"shadowCustomColor": "#000000",
|
||||
"clickThrough": false
|
||||
}
|
||||
@@ -1018,7 +977,6 @@ Singleton {
|
||||
loadSettings();
|
||||
initializeListModels();
|
||||
Processes.detectFprintd();
|
||||
Processes.detectU2f();
|
||||
Processes.checkPluginSettings();
|
||||
}
|
||||
}
|
||||
@@ -1068,7 +1026,6 @@ Singleton {
|
||||
elif command -v dconf >/dev/null 2>&1; then
|
||||
dconf read /org/gnome/desktop/interface/icon-theme 2>/dev/null | sed "s/'//g"
|
||||
fi`;
|
||||
|
||||
Proc.runCommand("detectCosmicIconTheme", ["sh", "-c", detectScript], (output, exitCode) => {
|
||||
if (exitCode !== 0)
|
||||
return;
|
||||
|
||||
@@ -673,232 +673,6 @@ Singleton {
|
||||
property color shadowMedium: Qt.rgba(0, 0, 0, 0.08)
|
||||
property color shadowStrong: Qt.rgba(0, 0, 0, 0.3)
|
||||
|
||||
readonly property bool elevationEnabled: typeof SettingsData !== "undefined" && (SettingsData.m3ElevationEnabled ?? true)
|
||||
readonly property real elevationBlurMax: typeof SettingsData !== "undefined" && SettingsData.m3ElevationIntensity !== undefined ? Math.min(128, Math.max(32, SettingsData.m3ElevationIntensity * 2)) : 64
|
||||
|
||||
readonly property real _elevMult: typeof SettingsData !== "undefined" && SettingsData.m3ElevationIntensity !== undefined ? SettingsData.m3ElevationIntensity / 12 : 1
|
||||
readonly property real _opMult: typeof SettingsData !== "undefined" && SettingsData.m3ElevationOpacity !== undefined ? SettingsData.m3ElevationOpacity / 60 : 1
|
||||
function normalizeElevationDirection(direction) {
|
||||
switch (direction) {
|
||||
case "top":
|
||||
case "topLeft":
|
||||
case "topRight":
|
||||
case "bottom":
|
||||
case "bottomLeft":
|
||||
case "bottomRight":
|
||||
case "left":
|
||||
case "right":
|
||||
case "autoBar":
|
||||
return direction;
|
||||
default:
|
||||
return "top";
|
||||
}
|
||||
}
|
||||
|
||||
readonly property string elevationLightDirection: {
|
||||
if (typeof SettingsData === "undefined" || !SettingsData.m3ElevationLightDirection)
|
||||
return "top";
|
||||
switch (SettingsData.m3ElevationLightDirection) {
|
||||
case "autoBar":
|
||||
case "top":
|
||||
case "topLeft":
|
||||
case "topRight":
|
||||
case "bottom":
|
||||
return SettingsData.m3ElevationLightDirection;
|
||||
default:
|
||||
return "top";
|
||||
}
|
||||
}
|
||||
readonly property real _elevDiagRatio: 0.55
|
||||
readonly property string _globalElevationDirForTokens: {
|
||||
const normalized = normalizeElevationDirection(elevationLightDirection);
|
||||
return normalized === "autoBar" ? "top" : normalized;
|
||||
}
|
||||
readonly property real _elevDirX: {
|
||||
switch (_globalElevationDirForTokens) {
|
||||
case "topLeft":
|
||||
case "bottomLeft":
|
||||
case "left":
|
||||
return 1;
|
||||
case "topRight":
|
||||
case "bottomRight":
|
||||
case "right":
|
||||
return -1;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
readonly property real _elevDirY: {
|
||||
switch (_globalElevationDirForTokens) {
|
||||
case "bottom":
|
||||
case "bottomLeft":
|
||||
case "bottomRight":
|
||||
return -1;
|
||||
case "left":
|
||||
case "right":
|
||||
return 0;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
readonly property real _elevDirXScale: (_globalElevationDirForTokens === "left" || _globalElevationDirForTokens === "right") ? 1 : _elevDiagRatio
|
||||
|
||||
readonly property var elevationLevel1: ({
|
||||
blurPx: 4 * _elevMult,
|
||||
offsetX: 1 * _elevMult * _elevDirXScale * _elevDirX,
|
||||
offsetY: 1 * _elevMult * _elevDirY,
|
||||
spreadPx: 0,
|
||||
alpha: 0.2 * _opMult
|
||||
})
|
||||
readonly property var elevationLevel2: ({
|
||||
blurPx: 8 * _elevMult,
|
||||
offsetX: 4 * _elevMult * _elevDirXScale * _elevDirX,
|
||||
offsetY: 4 * _elevMult * _elevDirY,
|
||||
spreadPx: 0,
|
||||
alpha: 0.25 * _opMult
|
||||
})
|
||||
readonly property var elevationLevel3: ({
|
||||
blurPx: 12 * _elevMult,
|
||||
offsetX: 6 * _elevMult * _elevDirXScale * _elevDirX,
|
||||
offsetY: 6 * _elevMult * _elevDirY,
|
||||
spreadPx: 0,
|
||||
alpha: 0.3 * _opMult
|
||||
})
|
||||
readonly property var elevationLevel4: ({
|
||||
blurPx: 16 * _elevMult,
|
||||
offsetX: 8 * _elevMult * _elevDirXScale * _elevDirX,
|
||||
offsetY: 8 * _elevMult * _elevDirY,
|
||||
spreadPx: 0,
|
||||
alpha: 0.3 * _opMult
|
||||
})
|
||||
readonly property var elevationLevel5: ({
|
||||
blurPx: 20 * _elevMult,
|
||||
offsetX: 10 * _elevMult * _elevDirXScale * _elevDirX,
|
||||
offsetY: 10 * _elevMult * _elevDirY,
|
||||
spreadPx: 0,
|
||||
alpha: 0.3 * _opMult
|
||||
})
|
||||
|
||||
function elevationOffsetMagnitude(level, fallback, direction) {
|
||||
if (!level) {
|
||||
return fallback !== undefined ? Math.abs(fallback) : 0;
|
||||
}
|
||||
const yMag = Math.abs(level.offsetY !== undefined ? level.offsetY : 0);
|
||||
if (yMag > 0)
|
||||
return yMag;
|
||||
const xMag = Math.abs(level.offsetX !== undefined ? level.offsetX : 0);
|
||||
if (xMag > 0) {
|
||||
if (direction === "left" || direction === "right")
|
||||
return xMag;
|
||||
return xMag / _elevDiagRatio;
|
||||
}
|
||||
return fallback !== undefined ? Math.abs(fallback) : 0;
|
||||
}
|
||||
|
||||
function elevationOffsetXFor(level, direction, fallback) {
|
||||
const dir = normalizeElevationDirection(direction || elevationLightDirection);
|
||||
const mag = elevationOffsetMagnitude(level, fallback, dir);
|
||||
switch (dir) {
|
||||
case "topLeft":
|
||||
case "bottomLeft":
|
||||
return mag * _elevDiagRatio;
|
||||
case "topRight":
|
||||
case "bottomRight":
|
||||
return -mag * _elevDiagRatio;
|
||||
case "left":
|
||||
return mag;
|
||||
case "right":
|
||||
return -mag;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function elevationOffsetYFor(level, direction, fallback) {
|
||||
const dir = normalizeElevationDirection(direction || elevationLightDirection);
|
||||
const mag = elevationOffsetMagnitude(level, fallback, dir);
|
||||
switch (dir) {
|
||||
case "bottom":
|
||||
case "bottomLeft":
|
||||
case "bottomRight":
|
||||
return -mag;
|
||||
case "left":
|
||||
case "right":
|
||||
return 0;
|
||||
default:
|
||||
return mag;
|
||||
}
|
||||
}
|
||||
|
||||
function elevationOffsetX(level, fallback) {
|
||||
return elevationOffsetXFor(level, elevationLightDirection, fallback);
|
||||
}
|
||||
|
||||
function elevationOffsetY(level, fallback) {
|
||||
return elevationOffsetYFor(level, elevationLightDirection, fallback);
|
||||
}
|
||||
|
||||
function elevationRenderPadding(level, direction, fallbackOffset, extraPadding, minPadding) {
|
||||
const dir = direction !== undefined ? direction : elevationLightDirection;
|
||||
const blur = (level && level.blurPx !== undefined) ? Math.max(0, level.blurPx) : 0;
|
||||
const spread = (level && level.spreadPx !== undefined) ? Math.max(0, level.spreadPx) : 0;
|
||||
const fallback = fallbackOffset !== undefined ? fallbackOffset : 0;
|
||||
const extra = extraPadding !== undefined ? extraPadding : 8;
|
||||
const minPad = minPadding !== undefined ? minPadding : 16;
|
||||
const offsetX = Math.abs(elevationOffsetXFor(level, dir, fallback));
|
||||
const offsetY = Math.abs(elevationOffsetYFor(level, dir, fallback));
|
||||
return Math.max(minPad, blur + spread + Math.max(offsetX, offsetY) + extra);
|
||||
}
|
||||
|
||||
function elevationShadowColor(level) {
|
||||
const alpha = (level && level.alpha !== undefined) ? level.alpha : 0.3;
|
||||
let r = 0;
|
||||
let g = 0;
|
||||
let b = 0;
|
||||
|
||||
if (typeof SettingsData !== "undefined") {
|
||||
const mode = SettingsData.m3ElevationColorMode || "default";
|
||||
if (mode === "default") {
|
||||
r = 0;
|
||||
g = 0;
|
||||
b = 0;
|
||||
} else if (mode === "text") {
|
||||
r = surfaceText.r;
|
||||
g = surfaceText.g;
|
||||
b = surfaceText.b;
|
||||
} else if (mode === "primary") {
|
||||
r = primary.r;
|
||||
g = primary.g;
|
||||
b = primary.b;
|
||||
} else if (mode === "surfaceVariant") {
|
||||
r = surfaceVariant.r;
|
||||
g = surfaceVariant.g;
|
||||
b = surfaceVariant.b;
|
||||
} else if (mode === "custom" && SettingsData.m3ElevationCustomColor) {
|
||||
const c = Qt.color(SettingsData.m3ElevationCustomColor);
|
||||
r = c.r;
|
||||
g = c.g;
|
||||
b = c.b;
|
||||
}
|
||||
}
|
||||
return Qt.rgba(r, g, b, alpha);
|
||||
}
|
||||
function elevationTintOpacity(level) {
|
||||
if (!level)
|
||||
return 0;
|
||||
if (level === elevationLevel1)
|
||||
return 0.05;
|
||||
if (level === elevationLevel2)
|
||||
return 0.08;
|
||||
if (level === elevationLevel3)
|
||||
return 0.11;
|
||||
if (level === elevationLevel4)
|
||||
return 0.12;
|
||||
if (level === elevationLevel5)
|
||||
return 0.14;
|
||||
return 0.08;
|
||||
}
|
||||
|
||||
readonly property var animationDurations: [
|
||||
{
|
||||
"shorter": 0,
|
||||
@@ -1084,7 +858,7 @@ Singleton {
|
||||
|
||||
property string fontFamily: {
|
||||
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
|
||||
return GreetdSettings.getEffectiveFontFamily();
|
||||
return GreetdSettings.fontFamily;
|
||||
}
|
||||
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
|
||||
}
|
||||
@@ -1187,6 +961,7 @@ Singleton {
|
||||
}
|
||||
|
||||
if (!isGreeterMode) {
|
||||
// Skip with matugen because, our script runner will do it.
|
||||
if (!matugenAvailable) {
|
||||
PortalService.setLightMode(light);
|
||||
}
|
||||
@@ -1395,23 +1170,21 @@ Singleton {
|
||||
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5;
|
||||
}
|
||||
|
||||
function barIconSize(barThickness, offset, maximizeIcon, iconScale) {
|
||||
function barIconSize(barThickness, offset, noBackground) {
|
||||
const defaultOffset = offset !== undefined ? offset : -6;
|
||||
const size = (maximizeIcon ?? false) ? iconSizeLarge : iconSize;
|
||||
const s = iconScale !== undefined ? iconScale : 1.0;
|
||||
const size = (noBackground ?? false) ? iconSizeLarge : iconSize;
|
||||
|
||||
return Math.round((barThickness / 48) * (size + defaultOffset) * s);
|
||||
return Math.round((barThickness / 48) * (size + defaultOffset));
|
||||
}
|
||||
|
||||
function barTextSize(barThickness, fontScale, maximizeText) {
|
||||
function barTextSize(barThickness, fontScale) {
|
||||
const scale = barThickness / 48;
|
||||
const dankBarScale = fontScale !== undefined ? fontScale : 1.0;
|
||||
const maxScale = (maximizeText ?? false) ? 1.5 : 1.0;
|
||||
if (scale <= 0.75)
|
||||
return Math.round(fontSizeSmall * 0.9 * dankBarScale * maxScale);
|
||||
return Math.round(fontSizeSmall * 0.9 * dankBarScale);
|
||||
if (scale >= 1.25)
|
||||
return Math.round(fontSizeMedium * dankBarScale * maxScale);
|
||||
return Math.round(fontSizeSmall * dankBarScale * maxScale);
|
||||
return Math.round(fontSizeMedium * dankBarScale);
|
||||
return Math.round(fontSizeSmall * dankBarScale);
|
||||
}
|
||||
|
||||
function getBatteryIcon(level, isCharging, batteryAvailable) {
|
||||
@@ -1987,7 +1760,7 @@ Singleton {
|
||||
FileView {
|
||||
id: dynamicColorsFileView
|
||||
path: {
|
||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
|
||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms";
|
||||
const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json";
|
||||
return colorsPath;
|
||||
}
|
||||
|
||||
@@ -18,10 +18,6 @@ Singleton {
|
||||
fprintdDetectionProcess.running = true;
|
||||
}
|
||||
|
||||
function detectU2f() {
|
||||
u2fDetectionProcess.running = true;
|
||||
}
|
||||
|
||||
function checkPluginSettings() {
|
||||
pluginSettingsCheckProcess.running = true;
|
||||
}
|
||||
@@ -52,7 +48,7 @@ Singleton {
|
||||
}
|
||||
|
||||
property var fprintdDetectionProcess: Process {
|
||||
command: ["sh", "-c", "command -v fprintd-list >/dev/null 2>&1 && fprintd-list \"${USER:-$(id -un)}\" >/dev/null 2>&1"]
|
||||
command: ["sh", "-c", "command -v fprintd-list >/dev/null 2>&1"]
|
||||
running: false
|
||||
onExited: function (exitCode) {
|
||||
if (!settingsRoot)
|
||||
@@ -61,16 +57,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
property var u2fDetectionProcess: Process {
|
||||
command: ["sh", "-c", "(test -f /usr/lib/security/pam_u2f.so || test -f /usr/lib64/security/pam_u2f.so) && (test -f /etc/pam.d/dankshell-u2f || test -f \"$HOME/.config/Yubico/u2f_keys\")"]
|
||||
running: false
|
||||
onExited: function (exitCode) {
|
||||
if (!settingsRoot)
|
||||
return;
|
||||
settingsRoot.u2fAvailable = (exitCode === 0);
|
||||
}
|
||||
}
|
||||
|
||||
property var pluginSettingsCheckProcess: Process {
|
||||
command: ["test", "-f", settingsRoot?.pluginSettingsPath || ""]
|
||||
running: false
|
||||
|
||||
@@ -77,14 +77,7 @@ var SPEC = {
|
||||
|
||||
deviceMaxVolumes: { def: {} },
|
||||
hiddenOutputDeviceNames: { def: [] },
|
||||
hiddenInputDeviceNames: { def: [] },
|
||||
|
||||
locale: { def: "", onChange: "updateLocale" },
|
||||
timeLocale: { def: "" },
|
||||
|
||||
launcherLastMode: { def: "all" },
|
||||
appDrawerLastMode: { def: "apps" },
|
||||
niriOverviewLastMode: { def: "apps" }
|
||||
hiddenInputDeviceNames: { def: [] }
|
||||
};
|
||||
|
||||
function getValidKeys() {
|
||||
|
||||
@@ -21,7 +21,7 @@ var SPEC = {
|
||||
widgetColorMode: { def: "default" },
|
||||
controlCenterTileColorMode: { def: "primary" },
|
||||
buttonColorMode: { def: "primary" },
|
||||
cornerRadius: { def: 16, onChange: "updateCompositorLayout" },
|
||||
cornerRadius: { def: 12, onChange: "updateCompositorLayout" },
|
||||
niriLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||
niriLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||
niriLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
||||
@@ -32,7 +32,6 @@ var SPEC = {
|
||||
mangoLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
||||
|
||||
firstDayOfWeek: { def: -1 },
|
||||
use24HourClock: { def: true },
|
||||
showSeconds: { def: false },
|
||||
padHours12Hour: { def: false },
|
||||
@@ -47,15 +46,6 @@ var SPEC = {
|
||||
modalAnimationSpeed: { def: 1 },
|
||||
modalCustomAnimationDuration: { def: 150 },
|
||||
enableRippleEffects: { def: true },
|
||||
m3ElevationEnabled: { def: true },
|
||||
m3ElevationIntensity: { def: 12 },
|
||||
m3ElevationOpacity: { def: 30 },
|
||||
m3ElevationColorMode: { def: "default" },
|
||||
m3ElevationLightDirection: { def: "top" },
|
||||
m3ElevationCustomColor: { def: "#000000" },
|
||||
modalElevationEnabled: { def: true },
|
||||
popoutElevationEnabled: { def: true },
|
||||
barElevationEnabled: { def: true },
|
||||
wallpaperFillMode: { def: "Fill" },
|
||||
blurredWallpaperLayer: { def: false },
|
||||
blurWallpaperOnOverview: { def: false },
|
||||
@@ -164,17 +154,6 @@ var SPEC = {
|
||||
centeringMode: { def: "index" },
|
||||
clockDateFormat: { def: "" },
|
||||
lockDateFormat: { def: "" },
|
||||
greeterRememberLastSession: { def: true },
|
||||
greeterRememberLastUser: { def: true },
|
||||
greeterEnableFprint: { def: false },
|
||||
greeterEnableU2f: { def: false },
|
||||
greeterWallpaperPath: { def: "" },
|
||||
greeterUse24HourClock: { def: true },
|
||||
greeterShowSeconds: { def: false },
|
||||
greeterPadHours12Hour: { def: false },
|
||||
greeterLockDateFormat: { def: "" },
|
||||
greeterFontFamily: { def: "" },
|
||||
greeterWallpaperFillMode: { def: "" },
|
||||
mediaSize: { def: 1 },
|
||||
|
||||
appLauncherViewMode: { def: "list" },
|
||||
@@ -338,15 +317,9 @@ var SPEC = {
|
||||
enableFprint: { def: false },
|
||||
maxFprintTries: { def: 15 },
|
||||
fprintdAvailable: { def: false, persist: false },
|
||||
enableU2f: { def: false },
|
||||
u2fMode: { def: "or" },
|
||||
u2fAvailable: { def: false, persist: false },
|
||||
lockScreenActiveMonitor: { def: "all" },
|
||||
lockScreenInactiveColor: { def: "#000000" },
|
||||
lockScreenNotificationMode: { def: 0 },
|
||||
lockScreenVideoEnabled: { def: false },
|
||||
lockScreenVideoPath: { def: "" },
|
||||
lockScreenVideoCycling: { def: false },
|
||||
hideBrightnessSlider: { def: false },
|
||||
|
||||
notificationTimeoutLow: { def: 5000 },
|
||||
@@ -368,7 +341,7 @@ var SPEC = {
|
||||
osdPosition: { def: 5 },
|
||||
osdVolumeEnabled: { def: true },
|
||||
osdMediaVolumeEnabled: { def: true },
|
||||
osdMediaPlaybackEnabled: { def: false },
|
||||
osdMediaPlaybackEnabled: { def: true },
|
||||
osdBrightnessEnabled: { def: true },
|
||||
osdIdleInhibitorEnabled: { def: true },
|
||||
osdMicMuteEnabled: { def: true },
|
||||
@@ -422,10 +395,6 @@ var SPEC = {
|
||||
widgetTransparency: 1.0,
|
||||
squareCorners: false,
|
||||
noBackground: false,
|
||||
maximizeWidgetIcons: false,
|
||||
maximizeWidgetText: false,
|
||||
removeWidgetPadding: false,
|
||||
widgetPadding: 8,
|
||||
gothCornersEnabled: false,
|
||||
gothCornerRadiusOverride: false,
|
||||
gothCornerRadiusValue: 12,
|
||||
@@ -438,7 +407,6 @@ var SPEC = {
|
||||
widgetOutlineOpacity: 1.0,
|
||||
widgetOutlineThickness: 1,
|
||||
fontScale: 1.0,
|
||||
iconScale: 1.0,
|
||||
autoHide: false,
|
||||
autoHideDelay: 250,
|
||||
showOnWindowsOpen: false,
|
||||
@@ -452,7 +420,7 @@ var SPEC = {
|
||||
scrollYBehavior: "workspace",
|
||||
shadowIntensity: 0,
|
||||
shadowOpacity: 60,
|
||||
shadowColorMode: "default",
|
||||
shadowColorMode: "text",
|
||||
shadowCustomColor: "#000000",
|
||||
clickThrough: false
|
||||
}], onChange: "updateBarConfigs"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user