mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-01 19:18:28 -04:00
greeter: migrated dms-greeter to github/AvengeMedia/dank-greeter
This commit is contained in:
@@ -33,8 +33,6 @@ jobs:
|
|||||||
- name: Run NixOS service start test
|
- name: Run NixOS service start test
|
||||||
run: nix build .#nixosTests.x86_64-linux.nixos-service-start-module -L
|
run: nix build .#nixosTests.x86_64-linux.nixos-service-start-module -L
|
||||||
|
|
||||||
- name: Run greeter niri test
|
|
||||||
run: nix build .#nixosTests.x86_64-linux.greeter-niri-module -L
|
|
||||||
|
|
||||||
- name: Run home-manager module test
|
- name: Run home-manager module test
|
||||||
run: nix build .#nixosTests.x86_64-linux.home-manager-module -L
|
run: nix build .#nixosTests.x86_64-linux.home-manager-module -L
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
package:
|
package:
|
||||||
description: 'Package to build (dms, dms-greeter, or both)'
|
description: 'Package to build'
|
||||||
required: false
|
required: false
|
||||||
default: 'dms'
|
default: 'dms'
|
||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- dms
|
- dms
|
||||||
- dms-greeter
|
|
||||||
- both
|
|
||||||
version:
|
version:
|
||||||
description: 'Versioning (e.g., 1.0.3, leave empty for latest release)'
|
description: 'Versioning (e.g., 1.0.3, leave empty for latest release)'
|
||||||
required: false
|
required: false
|
||||||
@@ -31,11 +29,7 @@ jobs:
|
|||||||
id: set-packages
|
id: set-packages
|
||||||
run: |
|
run: |
|
||||||
PACKAGE_INPUT="${{ github.event.inputs.package || 'dms' }}"
|
PACKAGE_INPUT="${{ github.event.inputs.package || 'dms' }}"
|
||||||
if [ "$PACKAGE_INPUT" = "both" ]; then
|
|
||||||
echo 'packages=["dms","dms-greeter"]' >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT
|
echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT
|
||||||
fi
|
|
||||||
|
|
||||||
build-and-upload:
|
build-and-upload:
|
||||||
needs: determine-packages
|
needs: determine-packages
|
||||||
@@ -163,8 +157,6 @@ jobs:
|
|||||||
PACKAGE="${{ matrix.package }}"
|
PACKAGE="${{ matrix.package }}"
|
||||||
if [ "$PACKAGE" = "dms" ]; then
|
if [ "$PACKAGE" = "dms" ]; then
|
||||||
COPR_PROJECT="avengemedia/dms"
|
COPR_PROJECT="avengemedia/dms"
|
||||||
elif [ "$PACKAGE" = "dms-greeter" ]; then
|
|
||||||
COPR_PROJECT="avengemedia/danklinux"
|
|
||||||
else
|
else
|
||||||
echo "❌ Unknown package: $PACKAGE"
|
echo "❌ Unknown package: $PACKAGE"
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ on:
|
|||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- dms
|
- dms
|
||||||
- dms-greeter
|
|
||||||
- dms-git
|
- dms-git
|
||||||
- all
|
- all
|
||||||
default: "dms"
|
default: "dms"
|
||||||
@@ -75,27 +74,12 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Helper function to check dms-greeter stable tag
|
|
||||||
check_dms_greeter_stable() {
|
|
||||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
|
||||||
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:danklinux/dms-greeter/dms-greeter.spec" 2>/dev/null || echo "")
|
|
||||||
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs | sed 's/^v//')
|
|
||||||
|
|
||||||
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "v$OBS_VERSION" ]]; then
|
|
||||||
echo "📋 dms-greeter: Tag $LATEST_TAG already exists, skipping"
|
|
||||||
return 1 # No update needed
|
|
||||||
else
|
|
||||||
echo "📋 dms-greeter: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
|
|
||||||
return 0 # Update needed
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main logic
|
# Main logic
|
||||||
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||||
|
|
||||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then
|
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then
|
||||||
# Run from tag with no package specified - update both stable packages
|
# Run from tag with no package specified - update stable package
|
||||||
echo "packages=dms dms-greeter" >> $GITHUB_OUTPUT
|
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
@@ -121,7 +105,7 @@ jobs:
|
|||||||
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
|
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
|
||||||
|
|
||||||
elif [[ "$PKG" == "all" ]]; then
|
elif [[ "$PKG" == "all" ]]; then
|
||||||
# Check each stable package and build list of those needing updates
|
# Check the stable package and build list of those needing updates
|
||||||
PACKAGES_TO_UPDATE=()
|
PACKAGES_TO_UPDATE=()
|
||||||
if check_dms_stable; then
|
if check_dms_stable; then
|
||||||
PACKAGES_TO_UPDATE+=("dms")
|
PACKAGES_TO_UPDATE+=("dms")
|
||||||
@@ -129,10 +113,6 @@ jobs:
|
|||||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if check_dms_greeter_stable; then
|
|
||||||
PACKAGES_TO_UPDATE+=("dms-greeter")
|
|
||||||
[[ -n "$LATEST_TAG" ]] && echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
||||||
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
||||||
@@ -141,7 +121,7 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo "packages=" >> $GITHUB_OUTPUT
|
echo "packages=" >> $GITHUB_OUTPUT
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
echo "✓ Both packages up to date"
|
echo "✓ All packages up to date"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
elif [[ "$PKG" == "dms-git" ]]; then
|
elif [[ "$PKG" == "dms-git" ]]; then
|
||||||
@@ -165,18 +145,6 @@ jobs:
|
|||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
elif [[ "$PKG" == "dms-greeter" ]]; then
|
|
||||||
if check_dms_greeter_stable; then
|
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
if [[ -n "$LATEST_TAG" ]]; then
|
|
||||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "packages=" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
else
|
else
|
||||||
# Unknown package - proceed anyway
|
# Unknown package - proceed anyway
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
@@ -221,9 +189,9 @@ jobs:
|
|||||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
# Manual workflow dispatch
|
# Manual workflow dispatch
|
||||||
|
|
||||||
# Determine version for dms stable and dms-greeter using the API
|
# Determine version for dms stable using the API
|
||||||
# GITHUB_REF is unreliable when "Use workflow from" a tag; API works from any ref
|
# 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
|
if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ 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 "")
|
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||||
if [[ -n "$LATEST_TAG" ]]; then
|
if [[ -n "$LATEST_TAG" ]]; then
|
||||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
@@ -289,7 +257,7 @@ jobs:
|
|||||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
||||||
} > "distro/debian/dms-git/debian/changelog"
|
} > "distro/debian/dms-git/debian/changelog"
|
||||||
|
|
||||||
- name: Update stable version (dms + dms-greeter)
|
- name: Update stable version (dms)
|
||||||
if: steps.packages.outputs.version != ''
|
if: steps.packages.outputs.version != ''
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.packages.outputs.version }}"
|
VERSION="${{ steps.packages.outputs.version }}"
|
||||||
@@ -325,25 +293,12 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Update dms-greeter changelog when dms-greeter is in the upload list
|
|
||||||
if [[ "$PACKAGES" == *"dms-greeter"* ]] && [[ -f "distro/debian/dms-greeter/debian/changelog" ]]; then
|
|
||||||
CHANGELOG_DATE=$(date -R)
|
|
||||||
{
|
|
||||||
echo "dms-greeter (${VERSION_NO_V}db1) unstable; urgency=medium"
|
|
||||||
echo ""
|
|
||||||
echo " * Update to $VERSION stable release"
|
|
||||||
echo ""
|
|
||||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
|
||||||
} > "distro/debian/dms-greeter/debian/changelog"
|
|
||||||
echo "✓ Updated dms-greeter changelog to ${VERSION_NO_V}db1"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update Debian _service files for packages in upload list (download_url paths)
|
# Update Debian _service files for packages in upload list (download_url paths)
|
||||||
for service in distro/debian/*/_service; do
|
for service in distro/debian/*/_service; do
|
||||||
if [[ -f "$service" ]]; then
|
if [[ -f "$service" ]]; then
|
||||||
# Update tar_scm revision parameter (for dms-git)
|
# Update tar_scm revision parameter (for dms-git)
|
||||||
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
||||||
# Update download_url paths (for dms, dms-greeter stable)
|
# Update download_url paths (for dms stable)
|
||||||
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
|
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
|
||||||
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
|
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
|
||||||
fi
|
fi
|
||||||
@@ -404,7 +359,7 @@ jobs:
|
|||||||
UPLOADED_PACKAGES=()
|
UPLOADED_PACKAGES=()
|
||||||
SKIPPED_PACKAGES=()
|
SKIPPED_PACKAGES=()
|
||||||
|
|
||||||
# PACKAGES can be space-separated list (e.g., "dms dms-greeter" from "all" check)
|
# PACKAGES can be a space-separated list (from the "all" check)
|
||||||
# Loop through each package and upload
|
# Loop through each package and upload
|
||||||
for PKG in $PACKAGES; do
|
for PKG in $PACKAGES; do
|
||||||
echo ""
|
echo ""
|
||||||
@@ -507,9 +462,6 @@ jobs:
|
|||||||
dms-git)
|
dms-git)
|
||||||
echo "- $STATUS_ICON **dms-git** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
|
echo "- $STATUS_ICON **dms-git** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
|
||||||
;;
|
;;
|
||||||
dms-greeter)
|
|
||||||
echo "- $STATUS_ICON **dms-greeter** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:danklinux/dms-greeter)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ on:
|
|||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- dms
|
- dms
|
||||||
- dms-greeter
|
|
||||||
- dms-git
|
- dms-git
|
||||||
- all
|
- all
|
||||||
default: "dms"
|
default: "dms"
|
||||||
@@ -135,7 +134,6 @@ jobs:
|
|||||||
case "$PACKAGE" in
|
case "$PACKAGE" in
|
||||||
dms) PPA_NAME="dms" ;;
|
dms) PPA_NAME="dms" ;;
|
||||||
dms-git) PPA_NAME="dms-git" ;;
|
dms-git) PPA_NAME="dms-git" ;;
|
||||||
dms-greeter) PPA_NAME="danklinux" ;;
|
|
||||||
*) echo "::error::Unknown package $PACKAGE"; exit 1 ;;
|
*) echo "::error::Unknown package $PACKAGE"; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@@ -150,4 +148,3 @@ jobs:
|
|||||||
echo "- **Target:** ${{ matrix.target }}" >> "$GITHUB_STEP_SUMMARY"
|
echo "- **Target:** ${{ matrix.target }}" >> "$GITHUB_STEP_SUMMARY"
|
||||||
echo "- **DMS PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> "$GITHUB_STEP_SUMMARY"
|
echo "- **DMS PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> "$GITHUB_STEP_SUMMARY"
|
||||||
echo "- **DMS-Git PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> "$GITHUB_STEP_SUMMARY"
|
echo "- **DMS-Git PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> "$GITHUB_STEP_SUMMARY"
|
||||||
echo "- **DankLinux PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
|
|||||||
@@ -27,11 +27,6 @@ on:
|
|||||||
type: boolean
|
type: boolean
|
||||||
required: false
|
required: false
|
||||||
default: true
|
default: true
|
||||||
build_greeter:
|
|
||||||
description: 'Build stable dms-greeter package'
|
|
||||||
type: boolean
|
|
||||||
required: false
|
|
||||||
default: true
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -106,7 +101,6 @@ jobs:
|
|||||||
- name: Inject templates
|
- name: Inject templates
|
||||||
run: |
|
run: |
|
||||||
cp -R distro/void/srcpkgs/dms void-packages/srcpkgs/
|
cp -R distro/void/srcpkgs/dms void-packages/srcpkgs/
|
||||||
cp -R distro/void/srcpkgs/dms-greeter void-packages/srcpkgs/
|
|
||||||
cp -R distro/void/srcpkgs/dms-git void-packages/srcpkgs/
|
cp -R distro/void/srcpkgs/dms-git void-packages/srcpkgs/
|
||||||
|
|
||||||
- name: Enable unprivileged user namespaces (Ubuntu 24.04)
|
- name: Enable unprivileged user namespaces (Ubuntu 24.04)
|
||||||
@@ -180,19 +174,16 @@ jobs:
|
|||||||
mkdir -p r2-repo/current
|
mkdir -p r2-repo/current
|
||||||
|
|
||||||
BUILD_DMS="${{ github.event.inputs.build_dms || 'true' }}"
|
BUILD_DMS="${{ github.event.inputs.build_dms || 'true' }}"
|
||||||
BUILD_GREETER="${{ github.event.inputs.build_greeter || 'true' }}"
|
|
||||||
BUILD_GIT="${{ github.event.inputs.build_git || 'true' }}"
|
BUILD_GIT="${{ github.event.inputs.build_git || 'true' }}"
|
||||||
FORCE_REBUILD="${{ github.event.inputs.force_rebuild || 'false' }}"
|
FORCE_REBUILD="${{ github.event.inputs.force_rebuild || 'false' }}"
|
||||||
|
|
||||||
if [ "${{ github.event_name }}" = "schedule" ]; then
|
if [ "${{ github.event_name }}" = "schedule" ]; then
|
||||||
BUILD_DMS="false"
|
BUILD_DMS="false"
|
||||||
BUILD_GREETER="false"
|
|
||||||
BUILD_GIT="true"
|
BUILD_GIT="true"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "${{ github.event_name }}" = "release" ]; then
|
if [ "${{ github.event_name }}" = "release" ]; then
|
||||||
BUILD_DMS="true"
|
BUILD_DMS="true"
|
||||||
BUILD_GREETER="true"
|
|
||||||
BUILD_GIT="false"
|
BUILD_GIT="false"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -217,23 +208,20 @@ jobs:
|
|||||||
|
|
||||||
echo "=== Starting Builds ==="
|
echo "=== Starting Builds ==="
|
||||||
echo "DMS stable build enabled: $BUILD_DMS"
|
echo "DMS stable build enabled: $BUILD_DMS"
|
||||||
echo "Greeter stable build enabled: $BUILD_GREETER"
|
|
||||||
echo "Git build enabled: $BUILD_GIT"
|
echo "Git build enabled: $BUILD_GIT"
|
||||||
echo "Force rebuild: $FORCE_REBUILD"
|
echo "Force rebuild: $FORCE_REBUILD"
|
||||||
|
|
||||||
cd void-packages
|
cd void-packages
|
||||||
|
|
||||||
if [ -n "$RELEASE_VER" ] && { [ "$BUILD_DMS" = "true" ] || [ "$BUILD_GREETER" = "true" ]; }; then
|
if [ -n "$RELEASE_VER" ] && [ "$BUILD_DMS" = "true" ]; then
|
||||||
echo "🔧 Updating stable templates for $ARCHIVE_TAG"
|
echo "🔧 Updating stable template for $ARCHIVE_TAG"
|
||||||
TARBALL="$(mktemp)"
|
TARBALL="$(mktemp)"
|
||||||
curl -fsSL -o "$TARBALL" "https://github.com/${{ github.repository }}/releases/download/${ARCHIVE_TAG}/dms-source.tar.gz"
|
curl -fsSL -o "$TARBALL" "https://github.com/${{ github.repository }}/releases/download/${ARCHIVE_TAG}/dms-source.tar.gz"
|
||||||
RELEASE_CHECKSUM="$(sha256sum "$TARBALL" | cut -d' ' -f1)"
|
RELEASE_CHECKSUM="$(sha256sum "$TARBALL" | cut -d' ' -f1)"
|
||||||
rm -f "$TARBALL"
|
rm -f "$TARBALL"
|
||||||
|
|
||||||
for pkg in dms dms-greeter; do
|
sed -i "s/^version=.*/version=${RELEASE_VER}/" srcpkgs/dms/template
|
||||||
sed -i "s/^version=.*/version=${RELEASE_VER}/" "srcpkgs/${pkg}/template"
|
sed -i "s/^checksum=.*/checksum=${RELEASE_CHECKSUM}/" srcpkgs/dms/template
|
||||||
sed -i "s/^checksum=.*/checksum=${RELEASE_CHECKSUM}/" "srcpkgs/${pkg}/template"
|
|
||||||
done
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 1. Build dms-git (development package)
|
# 1. Build dms-git (development package)
|
||||||
@@ -289,22 +277,6 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 3. Build stable dms-greeter package
|
|
||||||
if [ "$BUILD_GREETER" = "true" ]; then
|
|
||||||
GREETER_VER=$(grep -E '^version=' srcpkgs/dms-greeter/template | cut -d= -f2 | tr -d '"')
|
|
||||||
GREETER_REV=$(grep -E '^revision=' srcpkgs/dms-greeter/template | cut -d= -f2 | tr -d '"')
|
|
||||||
|
|
||||||
EXPECTED_GREETER_FILE="dms-greeter-${GREETER_VER}_${GREETER_REV}.x86_64.xbps"
|
|
||||||
if [ -f "../r2-repo/current/$EXPECTED_GREETER_FILE" ] && [ "$FORCE_REBUILD" != "true" ]; then
|
|
||||||
echo "✅ $EXPECTED_GREETER_FILE already exists, skipping build."
|
|
||||||
else
|
|
||||||
echo "🔨 Compiling dms-greeter ($GREETER_VER)..."
|
|
||||||
./xbps-src pkg dms-greeter
|
|
||||||
rm -f "../r2-repo/current/${EXPECTED_GREETER_FILE}"
|
|
||||||
cp -L hostdir/binpkgs/dms-greeter-${GREETER_VER}_${GREETER_REV}.x86_64.xbps ../r2-repo/current/
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Index and sign repository
|
- name: Index and sign repository
|
||||||
run: |
|
run: |
|
||||||
cd r2-repo/current
|
cd r2-repo/current
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ Notification center with grouping, rich text support, and keyboard navigation.
|
|||||||
MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews.
|
MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews.
|
||||||
|
|
||||||
**Session Management**
|
**Session Management**
|
||||||
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and greeter support.
|
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and a settings front-end for [dank-greeter](https://github.com/AvengeMedia/dank-greeter).
|
||||||
|
|
||||||
**Plugin System**
|
**Plugin System**
|
||||||
Extend functionality with the [plugin registry](https://plugins.danklinux.com).
|
Extend functionality with the [plugin registry](https://plugins.danklinux.com).
|
||||||
@@ -132,6 +132,17 @@ dms plugins search # Browse plugin registry
|
|||||||
- **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview)
|
- **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview)
|
||||||
- **Support:** [Ko-fi](https://ko-fi.com/avengemediallc)
|
- **Support:** [Ko-fi](https://ko-fi.com/avengemediallc)
|
||||||
|
|
||||||
|
## Dank Projects
|
||||||
|
|
||||||
|
DMS is one piece of the suite. The rest lives in its own repos:
|
||||||
|
|
||||||
|
- [dank-greeter](https://github.com/AvengeMedia/dank-greeter) - greetd login screen with the Dank Material aesthetic. The Greeter tab in DMS settings is the front-end for it.
|
||||||
|
- [dankcalendar](https://github.com/AvengeMedia/dankcalendar) - Local, Google, Microsoft, and CalDAV calendars for the dank desktop.
|
||||||
|
- [dgop](https://github.com/AvengeMedia/dgop) - System monitoring CLI and API that powers the process list and dashboard widgets.
|
||||||
|
- [dsearch](https://github.com/AvengeMedia/danksearch) - Fast file search that powers file results in the launcher.
|
||||||
|
- [dank-qml-common](https://github.com/AvengeMedia/dank-qml-common) - Shared QML widgets and components used by DMS, dank-greeter, and dankcalendar.
|
||||||
|
- [dankgo](https://github.com/AvengeMedia/dankgo) - Common Go modules behind the single binary apps.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
See component-specific documentation:
|
See component-specific documentation:
|
||||||
|
|||||||
+3
-3
@@ -78,7 +78,7 @@ Custom IPC via unix socket (JSON API) for shell communication.
|
|||||||
- `dms brightness [list|set]` - Control display/monitor brightness
|
- `dms brightness [list|set]` - Control display/monitor brightness
|
||||||
- `dms color pick` - Native color picker (see below)
|
- `dms color pick` - Native color picker (see below)
|
||||||
- `dms update` - Update DMS and dependencies (disabled in distro packages)
|
- `dms update` - Update DMS and dependencies (disabled in distro packages)
|
||||||
- `dms greeter install` - Install greetd greeter (disabled in distro packages)
|
- `dms greeter` - Deprecated; forwards to the standalone [dms-greeter](https://github.com/AvengeMedia/dank-greeter) binary
|
||||||
|
|
||||||
### Color Picker
|
### Color Picker
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ make test # Run tests
|
|||||||
**Distribution build:**
|
**Distribution build:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make dist # Build without update/greeter features
|
make dist # Build without update features
|
||||||
```
|
```
|
||||||
|
|
||||||
Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64`
|
Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64`
|
||||||
@@ -170,7 +170,7 @@ sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c hyprland -t ki
|
|||||||
| `--exclude-deps <name,...>` | | Skip specific dependencies |
|
| `--exclude-deps <name,...>` | | Skip specific dependencies |
|
||||||
| `--replace-configs <name,...>` | | Replace specific configuration files (mutually exclusive with `--replace-configs-all`) |
|
| `--replace-configs <name,...>` | | Replace specific configuration files (mutually exclusive with `--replace-configs-all`) |
|
||||||
| `--replace-configs-all` | | Replace all configuration files (mutually exclusive with `--replace-configs`) |
|
| `--replace-configs-all` | | Replace all configuration files (mutually exclusive with `--replace-configs`) |
|
||||||
| `--yes` | `-y` | Required for headless mode — confirms installation without interactive prompts |
|
| `--yes` | `-y` | Required for headless mode - confirms installation without interactive prompts |
|
||||||
|
|
||||||
Headless mode requires `--yes` to proceed; without it, the installer exits with an error.
|
Headless mode requires `--yes` to proceed; without it, the installer exits with an error.
|
||||||
Configuration files are not replaced by default unless `--replace-configs` or `--replace-configs-all` is specified.
|
Configuration files are not replaced by default unless `--replace-configs` or `--replace-configs-all` is specified.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ import (
|
|||||||
var authCmd = &cobra.Command{
|
var authCmd = &cobra.Command{
|
||||||
Use: "auth",
|
Use: "auth",
|
||||||
Short: "Manage DMS authentication sync",
|
Short: "Manage DMS authentication sync",
|
||||||
Long: "Manage shared PAM/authentication setup for DMS greeter and lock screen",
|
Long: "Manage PAM/authentication setup for the DMS lock screen",
|
||||||
}
|
}
|
||||||
|
|
||||||
var authSyncCmd = &cobra.Command{
|
var authSyncCmd = &cobra.Command{
|
||||||
@@ -211,3 +212,31 @@ func syncAuthInTerminal(nonInteractive bool) error {
|
|||||||
shellCmd := shellSyncCmd + `; echo; echo "Authentication sync finished. Closing in 3 seconds..."; sleep 3`
|
shellCmd := shellSyncCmd + `; echo; echo "Authentication sync finished. Closing in 3 seconds..."; sleep 3`
|
||||||
return runCommandInTerminal(shellCmd)
|
return runCommandInTerminal(shellCmd)
|
||||||
}
|
}
|
||||||
|
func runCommandInTerminal(shellCmd string) error {
|
||||||
|
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.Run(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("no terminal emulator found (tried: gnome-terminal, konsole, xfce4-terminal, ghostty, wezterm, alacritty, kitty, xterm)")
|
||||||
|
}
|
||||||
|
|||||||
+23
-2113
File diff suppressed because it is too large
Load Diff
@@ -1,150 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSyncGreeterConfigsAndAuthDelegatesSharedAuth(t *testing.T) {
|
|
||||||
origGreeterConfigSyncFn := greeterConfigSyncFn
|
|
||||||
origSharedAuthSyncFn := sharedAuthSyncFn
|
|
||||||
t.Cleanup(func() {
|
|
||||||
greeterConfigSyncFn = origGreeterConfigSyncFn
|
|
||||||
sharedAuthSyncFn = origSharedAuthSyncFn
|
|
||||||
})
|
|
||||||
|
|
||||||
var calls []string
|
|
||||||
greeterConfigSyncFn = func(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
|
||||||
if dmsPath != "/tmp/dms" {
|
|
||||||
t.Fatalf("unexpected dmsPath %q", dmsPath)
|
|
||||||
}
|
|
||||||
if compositor != "niri" {
|
|
||||||
t.Fatalf("unexpected compositor %q", compositor)
|
|
||||||
}
|
|
||||||
if sudoPassword != "" {
|
|
||||||
t.Fatalf("expected empty sudoPassword, got %q", sudoPassword)
|
|
||||||
}
|
|
||||||
calls = append(calls, "configs")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var gotOptions sharedpam.SyncAuthOptions
|
|
||||||
sharedAuthSyncFn = func(logFunc func(string), sudoPassword string, options sharedpam.SyncAuthOptions) error {
|
|
||||||
if sudoPassword != "" {
|
|
||||||
t.Fatalf("expected empty sudoPassword, got %q", sudoPassword)
|
|
||||||
}
|
|
||||||
gotOptions = options
|
|
||||||
calls = append(calls, "auth")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := syncGreeterConfigsAndAuth("/tmp/dms", "niri", func(string) {}, sharedpam.SyncAuthOptions{
|
|
||||||
ForceGreeterAuth: true,
|
|
||||||
}, func() {
|
|
||||||
calls = append(calls, "before-auth")
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("syncGreeterConfigsAndAuth returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
wantCalls := []string{"configs", "before-auth", "auth"}
|
|
||||||
if !reflect.DeepEqual(calls, wantCalls) {
|
|
||||||
t.Fatalf("call order = %v, want %v", calls, wantCalls)
|
|
||||||
}
|
|
||||||
if !gotOptions.ForceGreeterAuth {
|
|
||||||
t.Fatalf("expected ForceGreeterAuth to be true, got %+v", gotOptions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSyncGreeterConfigsAndAuthStopsOnConfigError(t *testing.T) {
|
|
||||||
origGreeterConfigSyncFn := greeterConfigSyncFn
|
|
||||||
origSharedAuthSyncFn := sharedAuthSyncFn
|
|
||||||
t.Cleanup(func() {
|
|
||||||
greeterConfigSyncFn = origGreeterConfigSyncFn
|
|
||||||
sharedAuthSyncFn = origSharedAuthSyncFn
|
|
||||||
})
|
|
||||||
|
|
||||||
greeterConfigSyncFn = func(string, string, func(string), string) error {
|
|
||||||
return errors.New("config sync failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
authCalled := false
|
|
||||||
sharedAuthSyncFn = func(func(string), string, sharedpam.SyncAuthOptions) error {
|
|
||||||
authCalled = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := syncGreeterConfigsAndAuth("/tmp/dms", "niri", func(string) {}, sharedpam.SyncAuthOptions{}, nil)
|
|
||||||
if err == nil || err.Error() != "config sync failed" {
|
|
||||||
t.Fatalf("expected config sync error, got %v", err)
|
|
||||||
}
|
|
||||||
if authCalled {
|
|
||||||
t.Fatal("expected auth sync not to run after config sync failure")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGreeterStatusStateDirUsesNixOSDefault(t *testing.T) {
|
|
||||||
if got := greeterStatusStateDir("", true); got != nixOSGreeterStateDir {
|
|
||||||
t.Fatalf("greeterStatusStateDir() = %q, want %q", got, nixOSGreeterStateDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGreeterStatusStateDirHonorsExplicitOverrideOnNixOS(t *testing.T) {
|
|
||||||
command := "dms-greeter --cache-dir /srv/dms-greeter --command niri"
|
|
||||||
if got := greeterStatusStateDir(command, true); got != "/srv/dms-greeter" {
|
|
||||||
t.Fatalf("greeterStatusStateDir() = %q, want %q", got, "/srv/dms-greeter")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtractGreeterWrapperFromEnvCommand(t *testing.T) {
|
|
||||||
command := "env LIBSEAT_BACKEND=logind DMS_VOID=1 /usr/bin/dms-greeter --command niri"
|
|
||||||
if got := extractGreeterWrapperFromCommand(command); got != "/usr/bin/dms-greeter" {
|
|
||||||
t.Fatalf("extractGreeterWrapperFromCommand() = %q, want %q", got, "/usr/bin/dms-greeter")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRejectNixOSGreeterMutationBlocksImperativeCommands(t *testing.T) {
|
|
||||||
origGreeterIsNixOSFn := greeterIsNixOSFn
|
|
||||||
greeterIsNixOSFn = func() bool { return true }
|
|
||||||
t.Cleanup(func() {
|
|
||||||
greeterIsNixOSFn = origGreeterIsNixOSFn
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, commandName := range []string{"install", "enable", "sync", "uninstall"} {
|
|
||||||
t.Run(commandName, func(t *testing.T) {
|
|
||||||
root := &cobra.Command{Use: "dms"}
|
|
||||||
greeterCommand := &cobra.Command{Use: "greeter"}
|
|
||||||
mutationCommand := &cobra.Command{Use: commandName}
|
|
||||||
root.AddCommand(greeterCommand)
|
|
||||||
greeterCommand.AddCommand(mutationCommand)
|
|
||||||
|
|
||||||
err := rejectNixOSGreeterMutation(mutationCommand)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected NixOS greeter %s to be rejected", commandName)
|
|
||||||
}
|
|
||||||
if !strings.Contains(err.Error(), "dms greeter "+commandName+" is disabled on NixOS") {
|
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if strings.Contains(err.Error(), "/var/cache/dms-greeter") {
|
|
||||||
t.Fatalf("NixOS remediation should not recommend the non-NixOS cache path: %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRejectNixOSGreeterMutationAllowsOtherDistros(t *testing.T) {
|
|
||||||
origGreeterIsNixOSFn := greeterIsNixOSFn
|
|
||||||
greeterIsNixOSFn = func() bool { return false }
|
|
||||||
t.Cleanup(func() {
|
|
||||||
greeterIsNixOSFn = origGreeterIsNixOSFn
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := rejectNixOSGreeterMutation(&cobra.Command{Use: "sync"}); err != nil {
|
|
||||||
t.Fatalf("expected non-NixOS greeter command to be allowed, got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
@@ -205,7 +204,12 @@ func detectTerminal() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func detectCompositorForSetup() (string, error) {
|
func detectCompositorForSetup() (string, error) {
|
||||||
compositors := greeter.DetectCompositors()
|
var compositors []string
|
||||||
|
for _, candidate := range []string{"niri", "Hyprland", "mango"} {
|
||||||
|
if utils.CommandExists(candidate) {
|
||||||
|
compositors = append(compositors, candidate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch len(compositors) {
|
switch len(compositors) {
|
||||||
case 0:
|
case 0:
|
||||||
@@ -214,11 +218,20 @@ func detectCompositorForSetup() (string, error) {
|
|||||||
return strings.ToLower(compositors[0]), nil
|
return strings.ToLower(compositors[0]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
selected, err := greeter.PromptCompositorChoice(compositors)
|
fmt.Println("Multiple compositors detected:")
|
||||||
if err != nil {
|
for i, compositor := range compositors {
|
||||||
return "", err
|
fmt.Printf("%d) %s\n", i+1, compositor)
|
||||||
}
|
}
|
||||||
return strings.ToLower(selected), nil
|
fmt.Printf("\nChoice (1-%d): ", len(compositors))
|
||||||
|
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
choice := 0
|
||||||
|
fmt.Sscanf(strings.TrimSpace(response), "%d", &choice)
|
||||||
|
if choice < 1 || choice > len(compositors) {
|
||||||
|
return "", fmt.Errorf("invalid choice")
|
||||||
|
}
|
||||||
|
return strings.ToLower(compositors[choice-1]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runSetupDmsConfig(name string) error {
|
func runSetupDmsConfig(name string) error {
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
)
|
|
||||||
|
|
||||||
// runit (Void Linux) service helpers. Services live in /etc/sv and are "enabled"
|
|
||||||
// by symlinking them into the /var/service supervision dir, so the greeter
|
|
||||||
// commands branch on isRunit() instead of shelling systemctl.
|
|
||||||
|
|
||||||
const (
|
|
||||||
runitSvDir = "/etc/sv"
|
|
||||||
runitServiceDir = "/var/service"
|
|
||||||
)
|
|
||||||
|
|
||||||
// isRunit reports whether this system is supervised by runit (Void Linux).
|
|
||||||
func isRunit() bool {
|
|
||||||
if fi, err := os.Stat("/run/runit"); err == nil && fi.IsDir() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if _, err := os.Stat("/run/systemd/system"); err == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if fi, err := os.Stat(runitServiceDir); err == nil && fi.IsDir() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func runitServiceInstalled(name string) bool {
|
|
||||||
fi, err := os.Stat(runitSvDir + "/" + name)
|
|
||||||
return err == nil && fi.IsDir()
|
|
||||||
}
|
|
||||||
|
|
||||||
func runitServiceEnabled(name string) bool {
|
|
||||||
_, err := os.Lstat(runitServiceDir + "/" + name)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// enableRunitService links a service into /var/service (idempotent).
|
|
||||||
func enableRunitService(name string) error {
|
|
||||||
if !runitServiceInstalled(name) {
|
|
||||||
return fmt.Errorf("runit service %q not found in %s", name, runitSvDir)
|
|
||||||
}
|
|
||||||
if runitServiceEnabled(name) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return privesc.Run(context.Background(), "", "ln", "-sf",
|
|
||||||
runitSvDir+"/"+name, runitServiceDir+"/"+name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// disableRunitService removes a service's supervision symlink.
|
|
||||||
func disableRunitService(name string) error {
|
|
||||||
if !runitServiceEnabled(name) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return privesc.Run(context.Background(), "", "rm", "-f",
|
|
||||||
runitServiceDir+"/"+name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensureRunitSeat sets up the seat access a Wayland greeter needs on runit (the
|
|
||||||
// equivalent of logind on systemd): enables seatd and adds the greeter user to
|
|
||||||
// the seat/video/input groups. Failures are reported but non-fatal.
|
|
||||||
func ensureRunitSeat(greeterUser string) {
|
|
||||||
if runitServiceInstalled("seatd") {
|
|
||||||
if err := enableRunitService("seatd"); err != nil {
|
|
||||||
fmt.Printf(" ⚠ could not enable seatd: %v\n", err)
|
|
||||||
} else {
|
|
||||||
fmt.Println(" ✓ seatd enabled")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Println(" ⚠ seatd not installed — the greeter compositor needs it for GPU/seat access")
|
|
||||||
}
|
|
||||||
if err := privesc.Run(context.Background(), "", "usermod", "-aG", "_seatd,video,input", greeterUser); err != nil {
|
|
||||||
fmt.Printf(" ⚠ could not add %s to seat groups: %v\n", greeterUser, err)
|
|
||||||
} else {
|
|
||||||
fmt.Printf(" ✓ %s added to seat groups (_seatd, video, input)\n", greeterUser)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensureVoidLogindGreeter configures the elogind-backed greeter on Void.
|
|
||||||
func ensureVoidLogindGreeter(greeterUser string) {
|
|
||||||
for _, service := range []string{"dbus", "elogind"} {
|
|
||||||
if err := enableRunitService(service); err != nil {
|
|
||||||
fmt.Printf(" ⚠ could not enable %s: %v\n", service, err)
|
|
||||||
} else {
|
|
||||||
fmt.Printf(" ✓ %s enabled\n", service)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
greeter.EnsureVoidGreetdRunScript(func(msg string) { fmt.Println(" " + msg) }, "")
|
|
||||||
if runitServiceEnabled("seatd") {
|
|
||||||
if err := disableRunitService("seatd"); err != nil {
|
|
||||||
fmt.Printf(" ⚠ could not disable seatd: %v\n", err)
|
|
||||||
} else {
|
|
||||||
fmt.Println(" ✓ seatd disabled (elogind manages the seat)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := privesc.Run(context.Background(), "", "usermod", "-aG", "video,input", greeterUser); err != nil {
|
|
||||||
fmt.Printf(" ⚠ could not add %s to video/input groups: %v\n", greeterUser, err)
|
|
||||||
} else {
|
|
||||||
fmt.Printf(" ✓ %s added to video/input groups (elogind manages the seat)\n", greeterUser)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensureGreetdPamRundir provides XDG_RUNTIME_DIR to runit greeter sessions.
|
|
||||||
func ensureGreetdPamRundir() {
|
|
||||||
const pamPath = "/etc/pam.d/greetd"
|
|
||||||
data, err := os.ReadFile(pamPath)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf(" ⚠ could not read %s: %v\n", pamPath, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if strings.Contains(string(data), "pam_rundir") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
line := "session optional pam_rundir.so"
|
|
||||||
if err := privesc.Run(context.Background(), "", "sh", "-c",
|
|
||||||
fmt.Sprintf("printf '%%s\\n' %q >> %s", line, pamPath)); err != nil {
|
|
||||||
fmt.Printf(" ⚠ could not add pam_rundir to %s: %v\n", pamPath, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Println(" ✓ pam_rundir added to greetd PAM (provides XDG_RUNTIME_DIR for the session)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// startGreeterHint returns the init-appropriate "start greetd now" command.
|
|
||||||
func startGreeterHint() string {
|
|
||||||
if isRunit() {
|
|
||||||
return " sudo sv up greetd"
|
|
||||||
}
|
|
||||||
return " sudo systemctl start greetd"
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd)
|
|
||||||
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
|
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
|
||||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
updateCmd.AddCommand(updateCheckCmd)
|
updateCmd.AddCommand(updateCheckCmd)
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd)
|
|
||||||
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
|
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
|
||||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
hotkey-overlay {
|
|
||||||
skip-at-startup
|
|
||||||
}
|
|
||||||
|
|
||||||
environment {
|
|
||||||
DMS_RUN_GREETER "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
gestures {
|
|
||||||
hot-corners {
|
|
||||||
off
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layout {
|
|
||||||
background-color "#000000"
|
|
||||||
}
|
|
||||||
@@ -16,6 +16,3 @@ var NiriAlttabConfig string
|
|||||||
|
|
||||||
//go:embed embedded/niri-binds.kdl
|
//go:embed embedded/niri-binds.kdl
|
||||||
var NiriBindsConfig string
|
var NiriBindsConfig string
|
||||||
|
|
||||||
//go:embed embedded/niri-greeter.kdl
|
|
||||||
var NiriGreeterConfig string
|
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
# AppArmor profile for dms-greeter
|
|
||||||
#
|
|
||||||
# Managed by DMS — regenerated on every `dms greeter install` / `dms greeter sync`.
|
|
||||||
# Manual edits will be overwritten on next sync.
|
|
||||||
#
|
|
||||||
# Mode: complain (denials are logged, nothing is blocked)
|
|
||||||
# To switch to enforce after validating with `aa-logprof`:
|
|
||||||
# sudo aa-enforce /etc/apparmor.d/usr.bin.dms-greeter
|
|
||||||
#
|
|
||||||
#include <tunables/global>
|
|
||||||
|
|
||||||
profile dms-greeter /usr/bin/dms-greeter flags=(complain) {
|
|
||||||
#include <abstractions/base>
|
|
||||||
#include <abstractions/bash>
|
|
||||||
|
|
||||||
# The launcher script itself
|
|
||||||
/usr/bin/dms-greeter r,
|
|
||||||
|
|
||||||
# Cache directory — created by dms greeter sync/enable with greeter:greeter ownership
|
|
||||||
/var/cache/dms-greeter/ rw,
|
|
||||||
/var/cache/dms-greeter/** rwlk,
|
|
||||||
|
|
||||||
# DMS config — packaged path
|
|
||||||
/usr/share/quickshell/dms-greeter/ r,
|
|
||||||
/usr/share/quickshell/dms-greeter/** r,
|
|
||||||
/usr/share/quickshell/ r,
|
|
||||||
/usr/share/quickshell/** r,
|
|
||||||
|
|
||||||
# DMS config — system and user overrides
|
|
||||||
/etc/dms/ r,
|
|
||||||
/etc/dms/** r,
|
|
||||||
/usr/share/dms/ r,
|
|
||||||
/usr/share/dms/** r,
|
|
||||||
/home/*/.config/quickshell/ r,
|
|
||||||
/home/*/.config/quickshell/** r,
|
|
||||||
/root/.config/quickshell/ r,
|
|
||||||
/root/.config/quickshell/** r,
|
|
||||||
|
|
||||||
# greetd / PAM — read-only for session setup
|
|
||||||
/etc/greetd/ r,
|
|
||||||
/etc/greetd/** r,
|
|
||||||
/etc/pam.d/ r,
|
|
||||||
/etc/pam.d/** r,
|
|
||||||
/usr/lib/pam.d/ r,
|
|
||||||
/usr/lib/pam.d/** r,
|
|
||||||
|
|
||||||
# Compositor binaries — run unconfined so each compositor uses its own profile
|
|
||||||
/usr/bin/niri Ux,
|
|
||||||
/usr/bin/hyprland Ux,
|
|
||||||
/usr/bin/Hyprland Ux,
|
|
||||||
/usr/bin/sway Ux,
|
|
||||||
/usr/bin/labwc Ux,
|
|
||||||
/usr/bin/scroll Ux,
|
|
||||||
/usr/bin/miracle-wm Ux,
|
|
||||||
/usr/bin/mango Ux,
|
|
||||||
|
|
||||||
# Quickshell — run unconfined (has its own compositor profile on some distros)
|
|
||||||
/usr/bin/qs Ux,
|
|
||||||
/usr/bin/quickshell Ux,
|
|
||||||
|
|
||||||
# Wayland / XDG runtime (pipewire, wireplumber, wayland socket)
|
|
||||||
/run/user/[0-9]*/ rw,
|
|
||||||
/run/user/[0-9]*/** rw,
|
|
||||||
|
|
||||||
# DRM / GPU devices (required for Wayland compositor startup)
|
|
||||||
/dev/dri/ r,
|
|
||||||
/dev/dri/* rw,
|
|
||||||
/dev/udmabuf rw,
|
|
||||||
|
|
||||||
# Input devices
|
|
||||||
/dev/input/ r,
|
|
||||||
/dev/input/* r,
|
|
||||||
|
|
||||||
# Systemd journal / logging
|
|
||||||
/run/systemd/journal/socket rw,
|
|
||||||
/dev/log rw,
|
|
||||||
|
|
||||||
# Shell helper binaries invoked by the launcher script
|
|
||||||
/usr/bin/env ix,
|
|
||||||
/usr/bin/mkdir ix,
|
|
||||||
/usr/bin/cat ix,
|
|
||||||
/usr/bin/grep ix,
|
|
||||||
/usr/bin/dirname ix,
|
|
||||||
/usr/bin/basename ix,
|
|
||||||
/usr/bin/command ix,
|
|
||||||
/bin/env ix,
|
|
||||||
/bin/mkdir ix,
|
|
||||||
|
|
||||||
# Signal management (compositor lifecycle)
|
|
||||||
signal (send, receive) set=("term", "int", "hup", "kill"),
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,350 +0,0 @@
|
|||||||
package greeter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func writeTestFile(t *testing.T, path string, content string) {
|
|
||||||
t.Helper()
|
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
||||||
t.Fatalf("failed to create parent dir for %s: %v", path, err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
|
||||||
t.Fatalf("failed to write %s: %v", path, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveGreeterThemeSyncState(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
settingsJSON string
|
|
||||||
sessionJSON string
|
|
||||||
wantSourcePath string
|
|
||||||
wantResolvedWallpaper string
|
|
||||||
wantDynamicOverrideUsed bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "dynamic theme with greeter wallpaper override uses generated greeter colors",
|
|
||||||
settingsJSON: `{
|
|
||||||
"currentThemeName": "dynamic",
|
|
||||||
"greeterWallpaperPath": "Pictures/blue.jpg",
|
|
||||||
"matugenScheme": "scheme-tonal-spot",
|
|
||||||
"iconTheme": "Papirus"
|
|
||||||
}`,
|
|
||||||
sessionJSON: `{"isLightMode":true}`,
|
|
||||||
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "greeter-colors", "dms-colors.json"),
|
|
||||||
wantResolvedWallpaper: filepath.Join("Pictures", "blue.jpg"),
|
|
||||||
wantDynamicOverrideUsed: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "dynamic theme without override uses desktop colors",
|
|
||||||
settingsJSON: `{
|
|
||||||
"currentThemeName": "dynamic",
|
|
||||||
"greeterWallpaperPath": ""
|
|
||||||
}`,
|
|
||||||
sessionJSON: `{"isLightMode":false}`,
|
|
||||||
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
|
|
||||||
wantResolvedWallpaper: "",
|
|
||||||
wantDynamicOverrideUsed: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "non-dynamic theme keeps desktop colors even with override wallpaper",
|
|
||||||
settingsJSON: `{
|
|
||||||
"currentThemeName": "purple",
|
|
||||||
"greeterWallpaperPath": "/tmp/blue.jpg"
|
|
||||||
}`,
|
|
||||||
sessionJSON: `{"isLightMode":false}`,
|
|
||||||
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
|
|
||||||
wantResolvedWallpaper: "/tmp/blue.jpg",
|
|
||||||
wantDynamicOverrideUsed: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
tt := tt
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
homeDir := t.TempDir()
|
|
||||||
writeTestFile(t, filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"), tt.settingsJSON)
|
|
||||||
writeTestFile(t, filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"), tt.sessionJSON)
|
|
||||||
|
|
||||||
state, err := resolveGreeterThemeSyncState(homeDir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("resolveGreeterThemeSyncState returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got := state.effectiveColorsSource(homeDir); got != filepath.Join(homeDir, tt.wantSourcePath) {
|
|
||||||
t.Fatalf("effectiveColorsSource = %q, want %q", got, filepath.Join(homeDir, tt.wantSourcePath))
|
|
||||||
}
|
|
||||||
|
|
||||||
wantResolvedWallpaper := tt.wantResolvedWallpaper
|
|
||||||
if wantResolvedWallpaper != "" && !filepath.IsAbs(wantResolvedWallpaper) {
|
|
||||||
wantResolvedWallpaper = filepath.Join(homeDir, wantResolvedWallpaper)
|
|
||||||
}
|
|
||||||
if state.ResolvedGreeterWallpaperPath != wantResolvedWallpaper {
|
|
||||||
t.Fatalf("ResolvedGreeterWallpaperPath = %q, want %q", state.ResolvedGreeterWallpaperPath, wantResolvedWallpaper)
|
|
||||||
}
|
|
||||||
|
|
||||||
if state.UsesDynamicWallpaperOverride != tt.wantDynamicOverrideUsed {
|
|
||||||
t.Fatalf("UsesDynamicWallpaperOverride = %v, want %v", state.UsesDynamicWallpaperOverride, tt.wantDynamicOverrideUsed)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUpsertInitialSession(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
baseConfig := `[terminal]
|
|
||||||
vt = 1
|
|
||||||
|
|
||||||
[default_session]
|
|
||||||
user = "greeter"
|
|
||||||
command = "/usr/bin/dms-greeter --command niri"
|
|
||||||
`
|
|
||||||
|
|
||||||
t.Run("inserts initial session", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
got := upsertInitialSession(baseConfig, "alice", "/var/cache/dms-greeter", true)
|
|
||||||
if !strings.Contains(got, "[initial_session]") {
|
|
||||||
t.Fatalf("expected [initial_session] section, got:\n%s", got)
|
|
||||||
}
|
|
||||||
if !strings.Contains(got, `user = "alice"`) {
|
|
||||||
t.Fatalf("expected alice user in initial session, got:\n%s", got)
|
|
||||||
}
|
|
||||||
if !strings.Contains(got, `dms greeter launch-session --from-memory --cache-dir`) {
|
|
||||||
t.Fatalf("expected stable launch-session command, got:\n%s", got)
|
|
||||||
}
|
|
||||||
if strings.Contains(got, `exec niri`) {
|
|
||||||
t.Fatalf("initial session must not bake the desktop Exec command, got:\n%s", got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("updates existing initial session", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
existing := baseConfig + `
|
|
||||||
[initial_session]
|
|
||||||
user = "bob"
|
|
||||||
command = "old-command"
|
|
||||||
`
|
|
||||||
got := upsertInitialSession(existing, "alice", "/var/cache/dms-greeter", true)
|
|
||||||
if strings.Contains(got, `user = "bob"`) {
|
|
||||||
t.Fatalf("expected bob to be replaced, got:\n%s", got)
|
|
||||||
}
|
|
||||||
if !strings.Contains(got, `dms greeter launch-session --from-memory`) {
|
|
||||||
t.Fatalf("expected launch-session command, got:\n%s", got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("removes initial session when disabled", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
existing := baseConfig + `
|
|
||||||
[initial_session]
|
|
||||||
user = "alice"
|
|
||||||
command = "niri"
|
|
||||||
`
|
|
||||||
got := upsertInitialSession(existing, "", "", false)
|
|
||||||
if strings.Contains(got, "[initial_session]") {
|
|
||||||
t.Fatalf("expected initial session removed, got:\n%s", got)
|
|
||||||
}
|
|
||||||
if !strings.Contains(got, "[default_session]") {
|
|
||||||
t.Fatalf("expected default session preserved, got:\n%s", got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStripDesktopExecCodes(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
got := stripDesktopExecCodes("niri --session %f")
|
|
||||||
want := "niri --session"
|
|
||||||
if got != want {
|
|
||||||
t.Fatalf("stripDesktopExecCodes = %q, want %q", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildGreetdCommand(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
wrapper string
|
|
||||||
compositor string
|
|
||||||
dmsPath string
|
|
||||||
useVoidLogind bool
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "standard command",
|
|
||||||
wrapper: "/usr/bin/dms-greeter",
|
|
||||||
compositor: "Niri",
|
|
||||||
want: "/usr/bin/dms-greeter --command niri --cache-dir /var/cache/dms-greeter",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "void selects elogind and keeps custom DMS path",
|
|
||||||
wrapper: "/usr/bin/dms-greeter",
|
|
||||||
compositor: "Niri",
|
|
||||||
dmsPath: "/usr/share/quickshell/dms-greeter",
|
|
||||||
useVoidLogind: true,
|
|
||||||
want: "env LIBSEAT_BACKEND=logind DMS_VOID=1 /usr/bin/dms-greeter --command niri --cache-dir /var/cache/dms-greeter -p /usr/share/quickshell/dms-greeter",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
if got := buildGreetdCommand(tt.wrapper, tt.compositor, tt.dmsPath, tt.useVoidLogind); got != tt.want {
|
|
||||||
t.Fatalf("buildGreetdCommand() = %q, want %q", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVoidLogindGreeterCommand(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
const oldCommand = "/usr/bin/dms-greeter --command niri -C /etc/greetd/niri.kdl"
|
|
||||||
const want = "env LIBSEAT_BACKEND=logind DMS_VOID=1 " + oldCommand
|
|
||||||
if got := voidLogindGreeterCommand(oldCommand); got != want {
|
|
||||||
t.Fatalf("voidLogindGreeterCommand() = %q, want %q", got, want)
|
|
||||||
}
|
|
||||||
if got := voidLogindGreeterCommand(want); got != want {
|
|
||||||
t.Fatalf("voidLogindGreeterCommand() must be idempotent, got %q", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveGreeterAutoLoginState(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cacheDir := t.TempDir()
|
|
||||||
homeDir := t.TempDir()
|
|
||||||
|
|
||||||
writeTestFile(t, filepath.Join(cacheDir, "settings.json"), `{
|
|
||||||
"greeterAutoLogin": true,
|
|
||||||
"greeterRememberLastUser": true,
|
|
||||||
"greeterRememberLastSession": true
|
|
||||||
}`)
|
|
||||||
writeTestFile(t, filepath.Join(cacheDir, ".local/state/memory.json"), `{
|
|
||||||
"lastSuccessfulUser": "alice",
|
|
||||||
"lastSessionDesktopId": "niri.desktop"
|
|
||||||
}`)
|
|
||||||
|
|
||||||
enabled, loginUser, sessionID, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("resolveGreeterAutoLoginState returned error: %v", err)
|
|
||||||
}
|
|
||||||
if !enabled || loginUser != "alice" || sessionID != "niri.desktop" {
|
|
||||||
t.Fatalf("got enabled=%v user=%q session=%q", enabled, loginUser, sessionID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveGreeterAutoLoginStateIgnoresStaleSessionExec(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cacheDir := t.TempDir()
|
|
||||||
homeDir := t.TempDir()
|
|
||||||
|
|
||||||
writeTestFile(t, filepath.Join(cacheDir, "settings.json"), `{
|
|
||||||
"greeterAutoLogin": true,
|
|
||||||
"greeterRememberLastUser": true,
|
|
||||||
"greeterRememberLastSession": true
|
|
||||||
}`)
|
|
||||||
writeTestFile(t, filepath.Join(cacheDir, ".local/state/memory.json"), `{
|
|
||||||
"lastSuccessfulUser": "alice",
|
|
||||||
"lastSessionId": "/nix/store/old-session/share/wayland-sessions/example.desktop",
|
|
||||||
"lastSessionExec": "/nix/store/old-session/bin/start-example-session"
|
|
||||||
}`)
|
|
||||||
|
|
||||||
enabled, loginUser, sessionID, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("resolveGreeterAutoLoginState returned error: %v", err)
|
|
||||||
}
|
|
||||||
if !enabled || loginUser != "alice" || sessionID != "example.desktop" {
|
|
||||||
t.Fatalf("got enabled=%v user=%q session=%q", enabled, loginUser, sessionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
got := upsertInitialSession("", loginUser, cacheDir, true)
|
|
||||||
if strings.Contains(got, "/nix/store/old-session") {
|
|
||||||
t.Fatalf("initial session must not include stale store path, got:\n%s", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveGreeterAutoLoginStateIgnoresMemoryFlag(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cacheDir := t.TempDir()
|
|
||||||
homeDir := t.TempDir()
|
|
||||||
|
|
||||||
writeTestFile(t, filepath.Join(cacheDir, "settings.json"), `{
|
|
||||||
"greeterAutoLogin": false,
|
|
||||||
"greeterRememberLastUser": true,
|
|
||||||
"greeterRememberLastSession": true
|
|
||||||
}`)
|
|
||||||
writeTestFile(t, filepath.Join(cacheDir, ".local/state/memory.json"), `{
|
|
||||||
"autoLoginEnabled": true,
|
|
||||||
"lastSuccessfulUser": "alice",
|
|
||||||
"lastSessionExec": "niri"
|
|
||||||
}`)
|
|
||||||
|
|
||||||
enabled, loginUser, sessionID, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("resolveGreeterAutoLoginState returned error: %v", err)
|
|
||||||
}
|
|
||||||
if enabled || loginUser != "" || sessionID != "" {
|
|
||||||
t.Fatalf("expected disabled with empty user/session, got enabled=%v user=%q session=%q", enabled, loginUser, sessionID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveSessionExecInDirs(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
oldDir := filepath.Join(t.TempDir(), "wayland-sessions")
|
|
||||||
newDir := filepath.Join(t.TempDir(), "wayland-sessions")
|
|
||||||
writeTestFile(t, filepath.Join(oldDir, "example.desktop"), `[Desktop Entry]
|
|
||||||
Name=Example Session
|
|
||||||
Exec=/nix/store/old-session/bin/start-example-session
|
|
||||||
`)
|
|
||||||
writeTestFile(t, filepath.Join(newDir, "example.desktop"), `[Desktop Entry]
|
|
||||||
Name=Example Session
|
|
||||||
Exec=/run/current-system/sw/bin/start-example-session
|
|
||||||
`)
|
|
||||||
|
|
||||||
got, err := resolveSessionExecInDirs("example.desktop", []string{newDir, oldDir})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("resolveSessionExecInDirs returned error: %v", err)
|
|
||||||
}
|
|
||||||
if got != "/run/current-system/sw/bin/start-example-session" {
|
|
||||||
t.Fatalf("resolveSessionExecInDirs = %q", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClearGreeterAutoLoginMemory(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
memoryPath := filepath.Join(t.TempDir(), "memory.json")
|
|
||||||
writeTestFile(t, memoryPath, `{
|
|
||||||
"autoLoginEnabled": true,
|
|
||||||
"lastSuccessfulUser": "alice"
|
|
||||||
}`)
|
|
||||||
|
|
||||||
if err := clearGreeterAutoLoginMemory(memoryPath, ""); err != nil {
|
|
||||||
t.Fatalf("clearGreeterAutoLoginMemory returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := os.ReadFile(memoryPath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read memory file: %v", err)
|
|
||||||
}
|
|
||||||
if strings.Contains(string(data), "autoLoginEnabled") {
|
|
||||||
t.Fatalf("expected autoLoginEnabled removed, got: %s", string(data))
|
|
||||||
}
|
|
||||||
if !strings.Contains(string(data), "lastSuccessfulUser") {
|
|
||||||
t.Fatalf("expected other memory fields preserved, got: %s", string(data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
package greeter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
func sessionDesktopIDFromPath(path string) string {
|
|
||||||
id := strings.TrimSpace(path)
|
|
||||||
if id == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if strings.ContainsAny(id, "/\\") {
|
|
||||||
id = filepath.Base(id)
|
|
||||||
}
|
|
||||||
if id == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if !strings.HasSuffix(id, ".desktop") {
|
|
||||||
id += ".desktop"
|
|
||||||
}
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
func sessionDesktopIDFromMemory(mem greeterAutoLoginMemory) string {
|
|
||||||
if id := sessionDesktopIDFromPath(mem.LastSessionDesktopID); id != "" {
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
return sessionDesktopIDFromPath(mem.LastSessionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sessionDesktopDirs() []string {
|
|
||||||
seen := make(map[string]bool)
|
|
||||||
dirs := make([]string, 0, 8)
|
|
||||||
|
|
||||||
addBase := func(base string) {
|
|
||||||
base = strings.TrimSpace(base)
|
|
||||||
if base == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, sub := range []string{"wayland-sessions", "xsessions"} {
|
|
||||||
dir := filepath.Join(base, sub)
|
|
||||||
if seen[dir] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[dir] = true
|
|
||||||
dirs = append(dirs, dir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if dataHome := os.Getenv("XDG_DATA_HOME"); dataHome != "" {
|
|
||||||
addBase(dataHome)
|
|
||||||
} else if home, err := os.UserHomeDir(); err == nil && home != "" {
|
|
||||||
addBase(filepath.Join(home, ".local", "share"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if dataDirs := os.Getenv("XDG_DATA_DIRS"); dataDirs != "" {
|
|
||||||
for _, dir := range strings.Split(dataDirs, ":") {
|
|
||||||
addBase(dir)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
addBase("/usr/local/share")
|
|
||||||
addBase("/usr/share")
|
|
||||||
}
|
|
||||||
|
|
||||||
return dirs
|
|
||||||
}
|
|
||||||
|
|
||||||
func ResolveSessionExec(sessionID string) (string, error) {
|
|
||||||
return resolveSessionExecInDirs(sessionID, sessionDesktopDirs())
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveSessionExecInDirs(sessionID string, dirs []string) (string, error) {
|
|
||||||
id := sessionDesktopIDFromPath(sessionID)
|
|
||||||
if id == "" {
|
|
||||||
return "", fmt.Errorf("session id is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dir := range dirs {
|
|
||||||
path := filepath.Join(dir, id)
|
|
||||||
execLine, err := execFromDesktopFile(path)
|
|
||||||
if err == nil {
|
|
||||||
return execLine, nil
|
|
||||||
}
|
|
||||||
if !os.IsNotExist(err) {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("session desktop file %q was not found", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseExecString splits a Desktop Entry Exec= value into argv without
|
|
||||||
// involving a shell, mirroring quickshell's DesktopEntry::parseExecString
|
|
||||||
// (string quoting, value escapes, field code stripping).
|
|
||||||
func parseExecString(execLine string) []string {
|
|
||||||
var args []string
|
|
||||||
var cur strings.Builder
|
|
||||||
inString := false
|
|
||||||
escape := 0
|
|
||||||
percent := false
|
|
||||||
|
|
||||||
for _, c := range execLine {
|
|
||||||
switch {
|
|
||||||
case escape == 0 && c == '\\':
|
|
||||||
escape = 1
|
|
||||||
case inString:
|
|
||||||
switch {
|
|
||||||
case c == '\\':
|
|
||||||
escape++
|
|
||||||
if escape == 4 {
|
|
||||||
cur.WriteByte('\\')
|
|
||||||
escape = 0
|
|
||||||
}
|
|
||||||
case escape == 2:
|
|
||||||
cur.WriteRune(c)
|
|
||||||
escape = 0
|
|
||||||
case escape != 0:
|
|
||||||
switch c {
|
|
||||||
case 's':
|
|
||||||
cur.WriteByte(' ')
|
|
||||||
case 'n':
|
|
||||||
cur.WriteByte('\n')
|
|
||||||
case 't':
|
|
||||||
cur.WriteByte('\t')
|
|
||||||
case 'r':
|
|
||||||
cur.WriteByte('\r')
|
|
||||||
default:
|
|
||||||
cur.WriteRune(c)
|
|
||||||
}
|
|
||||||
escape = 0
|
|
||||||
case c == '"' || c == '\'':
|
|
||||||
inString = false
|
|
||||||
default:
|
|
||||||
cur.WriteRune(c)
|
|
||||||
}
|
|
||||||
case escape != 0:
|
|
||||||
cur.WriteRune(c)
|
|
||||||
escape = 0
|
|
||||||
case percent:
|
|
||||||
if c == '%' {
|
|
||||||
cur.WriteByte('%')
|
|
||||||
}
|
|
||||||
percent = false
|
|
||||||
case c == '%':
|
|
||||||
percent = true
|
|
||||||
case c == '"' || c == '\'':
|
|
||||||
inString = true
|
|
||||||
case c == ' ':
|
|
||||||
if cur.Len() > 0 {
|
|
||||||
args = append(args, cur.String())
|
|
||||||
cur.Reset()
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
cur.WriteRune(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if cur.Len() > 0 {
|
|
||||||
args = append(args, cur.String())
|
|
||||||
}
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
|
|
||||||
func LaunchSessionByID(sessionID string) error {
|
|
||||||
execLine, err := ResolveSessionExec(sessionID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
argv := parseExecString(strings.TrimSpace(execLine))
|
|
||||||
if len(argv) == 0 {
|
|
||||||
return fmt.Errorf("session %q has an empty Exec command", sessionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
resolved, err := exec.LookPath(argv[0])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("session %q command %q not found: %w", sessionID, argv[0], err)
|
|
||||||
}
|
|
||||||
|
|
||||||
env := append(os.Environ(), "XDG_SESSION_TYPE=wayland")
|
|
||||||
return syscall.Exec(resolved, argv, env)
|
|
||||||
}
|
|
||||||
|
|
||||||
func LaunchSessionFromMemory(cacheDir, homeDir string) error {
|
|
||||||
enabled, _, sessionID, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !enabled {
|
|
||||||
return fmt.Errorf("greeter auto-login is disabled")
|
|
||||||
}
|
|
||||||
if sessionID == "" {
|
|
||||||
return fmt.Errorf("greeter auto-login has no remembered session")
|
|
||||||
}
|
|
||||||
return LaunchSessionByID(sessionID)
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package greeter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseExecString(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
exec string
|
|
||||||
want []string
|
|
||||||
}{
|
|
||||||
{"plain", "niri --session", []string{"niri", "--session"}},
|
|
||||||
{"extra spaces", "niri --session", []string{"niri", "--session"}},
|
|
||||||
{"double quoted arg", `env "with space" run`, []string{"env", "with space", "run"}},
|
|
||||||
{"single quoted arg", `env 'with space' run`, []string{"env", "with space", "run"}},
|
|
||||||
{"escaped quote in quotes", `sh "say \\"hi\\""`, []string{"sh", `say "hi"`}},
|
|
||||||
{"field code dropped", "gnome-session %U", []string{"gnome-session"}},
|
|
||||||
{"field code mid-arg", "app --url=%u --run", []string{"app", "--url=", "--run"}},
|
|
||||||
{"literal percent", "app 100%% done", []string{"app", "100%", "done"}},
|
|
||||||
{"shell metachars stay literal", "sh -c $(reboot); echo", []string{"sh", "-c", "$(reboot);", "echo"}},
|
|
||||||
{"empty", "", nil},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if got := parseExecString(tt.exec); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Fatalf("parseExecString(%q) = %#v, want %#v", tt.exec, got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExecFromDesktopFileOnlyReadsDesktopEntryGroup(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
path := filepath.Join(t.TempDir(), "example.desktop")
|
|
||||||
writeTestFile(t, path, `[Desktop Action other]
|
|
||||||
Exec=/wrong/binary
|
|
||||||
|
|
||||||
[Desktop Entry]
|
|
||||||
Name=Example
|
|
||||||
Exec = /right/binary --flag
|
|
||||||
`)
|
|
||||||
|
|
||||||
got, err := execFromDesktopFile(path)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("execFromDesktopFile returned error: %v", err)
|
|
||||||
}
|
|
||||||
if got != "/right/binary --flag" {
|
|
||||||
t.Fatalf("execFromDesktopFile = %q, want %q", got, "/right/binary --flag")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,548 +0,0 @@
|
|||||||
package greeter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"os/user"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
var monitorWallpaperSanitizer = regexp.MustCompile(`[^a-zA-Z0-9]+`)
|
|
||||||
|
|
||||||
func userGreeterCacheDir(cacheDir, username string) string {
|
|
||||||
return filepath.Join(cacheDir, "users", username)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isUserOwnedGreeterCacheSlot(path, username string) bool {
|
|
||||||
if strings.TrimSpace(username) == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
userDir, err := filepath.Abs(userGreeterCacheDir(GreeterCacheDir, username))
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
abs, err := filepath.Abs(path)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return abs == userDir || strings.HasPrefix(abs, userDir+string(filepath.Separator))
|
|
||||||
}
|
|
||||||
|
|
||||||
func UserIsInGreeterGroup(username string) bool {
|
|
||||||
group := DetectGreeterGroup()
|
|
||||||
if !utils.HasGroup(group) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
groupsCmd := exec.Command("groups", username)
|
|
||||||
groupsOutput, err := groupsCmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return strings.Contains(string(groupsOutput), group)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CanSyncOwnUserGreeterProfile(username string) bool {
|
|
||||||
currentUser, err := user.Current()
|
|
||||||
if err != nil || currentUser.Username != username {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !UserIsInGreeterGroup(username) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
usersDir := filepath.Join(GreeterCacheDir, "users")
|
|
||||||
if st, err := os.Stat(usersDir); err != nil || !st.IsDir() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
testFile := filepath.Join(usersDir, ".write-test-"+username)
|
|
||||||
file, err := os.OpenFile(testFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o660)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_ = file.Close()
|
|
||||||
_ = os.Remove(testFile)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func GreeterProfileSyncReady() bool {
|
|
||||||
if command := readGreeterSessionCommand(); command != "" && strings.Contains(command, "dms-greeter") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
usersDir := filepath.Join(GreeterCacheDir, "users")
|
|
||||||
st, err := os.Stat(usersDir)
|
|
||||||
return err == nil && st.IsDir()
|
|
||||||
}
|
|
||||||
|
|
||||||
func readGreeterSessionCommand() string {
|
|
||||||
data, err := os.ReadFile("/etc/greetd/config.toml")
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
inDefaultSession := false
|
|
||||||
for line := range strings.SplitSeq(string(data), "\n") {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
|
|
||||||
inDefaultSession = strings.EqualFold(strings.Trim(trimmed, "[]"), "default_session")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !inDefaultSession {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if idx := strings.Index(trimmed, "#"); idx >= 0 {
|
|
||||||
trimmed = strings.TrimSpace(trimmed[:idx])
|
|
||||||
}
|
|
||||||
if !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 ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// SyncUserProfileCache writes the current user's theme slot under users/<username>/
|
|
||||||
// without modifying greetd or other system configuration. Requires membership in the
|
|
||||||
// greeter group and a prior full greeter setup by an administrator.
|
|
||||||
func SyncUserProfileCache(logFunc func(string)) error {
|
|
||||||
if logFunc == nil {
|
|
||||||
logFunc = func(string) {}
|
|
||||||
}
|
|
||||||
if !GreeterProfileSyncReady() {
|
|
||||||
return fmt.Errorf("greeter is not set up on this system yet; an administrator must run 'dms greeter install' or 'dms greeter sync' once first")
|
|
||||||
}
|
|
||||||
|
|
||||||
currentUser, err := user.Current()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to resolve current user: %w", err)
|
|
||||||
}
|
|
||||||
if !CanSyncOwnUserGreeterProfile(currentUser.Username) {
|
|
||||||
group := DetectGreeterGroup()
|
|
||||||
return fmt.Errorf("cannot sync greeter profile: you must be in the %s group with write access to %s/users\nAsk an administrator to run:\n sudo usermod -aG %s %s\nThen log out and back in before running:\n dms greeter sync --profile",
|
|
||||||
group, GreeterCacheDir, group, currentUser.Username)
|
|
||||||
}
|
|
||||||
|
|
||||||
homeDir, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
state, err := resolveGreeterThemeSyncState(homeDir)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to resolve greeter color source: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := syncUserGreeterCacheSlot(homeDir, GreeterCacheDir, currentUser.Username, state, logFunc, userSlotSyncOpts{
|
|
||||||
profileOnly: true,
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logFunc(fmt.Sprintf(" → %s/users/%s/", GreeterCacheDir, currentUser.Username))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func canWriteUserGreeterCacheSlot(dest, username string) bool {
|
|
||||||
return isUserOwnedGreeterCacheSlot(dest, username) && CanSyncOwnUserGreeterProfile(username)
|
|
||||||
}
|
|
||||||
|
|
||||||
type userSlotSyncOpts struct {
|
|
||||||
sudoPassword string
|
|
||||||
profileOnly bool
|
|
||||||
username string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o userSlotSyncOpts) useDirectWrite(dest string) bool {
|
|
||||||
if !o.profileOnly {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return canWriteUserGreeterCacheSlot(dest, o.username)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isGreeterCachePath(path string) bool {
|
|
||||||
abs, err := filepath.Abs(path)
|
|
||||||
if err != nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
cacheAbs, err := filepath.Abs(GreeterCacheDir)
|
|
||||||
if err != nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if abs == cacheAbs {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return strings.HasPrefix(abs, cacheAbs+string(filepath.Separator))
|
|
||||||
}
|
|
||||||
|
|
||||||
func greeterCacheOwner() string {
|
|
||||||
greeterGroup := DetectGreeterGroup()
|
|
||||||
daemonUser := DetectGreeterUser()
|
|
||||||
return daemonUser + ":" + greeterGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureGreeterCacheSubdir(dir string, opts userSlotSyncOpts) error {
|
|
||||||
if opts.useDirectWrite(dir) {
|
|
||||||
if err := os.MkdirAll(dir, 0o770); err != nil {
|
|
||||||
return fmt.Errorf("failed to create cache directory %s: %w", dir, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := privesc.Run(context.Background(), opts.sudoPassword, "mkdir", "-p", dir); err != nil {
|
|
||||||
return fmt.Errorf("failed to create cache directory %s: %w", dir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
owner := greeterCacheOwner()
|
|
||||||
if err := privesc.Run(context.Background(), opts.sudoPassword, "chown", owner, dir); err != nil {
|
|
||||||
if fallbackErr := privesc.Run(context.Background(), opts.sudoPassword, "chown", "root:"+DetectGreeterGroup(), dir); fallbackErr != nil {
|
|
||||||
return fmt.Errorf("failed to set ownership on %s: %w", dir, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := privesc.Run(context.Background(), opts.sudoPassword, "chmod", "2770", dir); err != nil {
|
|
||||||
return fmt.Errorf("failed to set permissions on %s: %w", dir, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setGreeterCacheFileOwnership(path, sudoPassword string) error {
|
|
||||||
owner := greeterCacheOwner()
|
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "chown", owner, path); err != nil {
|
|
||||||
if fallbackErr := privesc.Run(context.Background(), sudoPassword, "chown", "root:"+DetectGreeterGroup(), path); fallbackErr != nil {
|
|
||||||
return fmt.Errorf("failed to set ownership on %s: %w", path, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", path); err != nil {
|
|
||||||
return fmt.Errorf("failed to set permissions on %s: %w", path, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncUserGreeterCacheSlot(homeDir, cacheDir, username string, state greeterThemeSyncState, logFunc func(string), opts userSlotSyncOpts) error {
|
|
||||||
if strings.TrimSpace(username) == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
opts.username = username
|
|
||||||
|
|
||||||
userDir := userGreeterCacheDir(cacheDir, username)
|
|
||||||
if err := ensureGreeterCacheSubdir(userDir, opts); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
|
|
||||||
settingsBytes, err := os.ReadFile(settingsPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read settings for user cache slot: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsMap := map[string]any{}
|
|
||||||
if strings.TrimSpace(string(settingsBytes)) != "" {
|
|
||||||
if err := json.Unmarshal(settingsBytes, &settingsMap); err != nil {
|
|
||||||
return fmt.Errorf("failed to parse settings for user cache slot: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if customTheme, ok := settingsMap["customThemeFile"].(string); ok && strings.TrimSpace(customTheme) != "" {
|
|
||||||
resolvedTheme := customTheme
|
|
||||||
if !filepath.IsAbs(resolvedTheme) {
|
|
||||||
resolvedTheme = filepath.Join(homeDir, resolvedTheme)
|
|
||||||
}
|
|
||||||
if st, statErr := os.Stat(resolvedTheme); statErr == nil && !st.IsDir() {
|
|
||||||
destTheme := filepath.Join(userDir, "custom-theme.json")
|
|
||||||
if err := copyFileWithPrivesc(resolvedTheme, destTheme, opts); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
settingsMap["customThemeFile"] = destTheme
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsBytes, err = json.Marshal(settingsMap)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to marshal settings for user cache slot: %w", err)
|
|
||||||
}
|
|
||||||
if err := writeFileWithPrivesc(filepath.Join(userDir, "settings.json"), settingsBytes, opts); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionPath := filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json")
|
|
||||||
sessionBytes, err := os.ReadFile(sessionPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read session for user cache slot: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionMap := map[string]any{}
|
|
||||||
if strings.TrimSpace(string(sessionBytes)) != "" {
|
|
||||||
if err := json.Unmarshal(sessionBytes, &sessionMap); err != nil {
|
|
||||||
return fmt.Errorf("failed to parse session for user cache slot: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := localizeSessionWallpapers(sessionMap, userDir, opts); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionBytes, err = json.Marshal(sessionMap)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to marshal session for user cache slot: %w", err)
|
|
||||||
}
|
|
||||||
if err := writeFileWithPrivesc(filepath.Join(userDir, "session.json"), sessionBytes, opts); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
colorsSource := state.effectiveColorsSource(homeDir)
|
|
||||||
if err := copyFileWithPrivesc(colorsSource, filepath.Join(userDir, "colors.json"), opts); err != nil {
|
|
||||||
return fmt.Errorf("failed to copy colors for user cache slot: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := syncUserProfileImage(homeDir, userDir, opts); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
rootOverride := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
|
|
||||||
userOverride := filepath.Join(userDir, "greeter_wallpaper_override.jpg")
|
|
||||||
if st, statErr := os.Stat(rootOverride); statErr == nil && !st.IsDir() {
|
|
||||||
if err := copyFileWithPrivesc(rootOverride, userOverride, opts); err != nil {
|
|
||||||
return fmt.Errorf("failed to copy greeter wallpaper override for user cache slot: %w", err)
|
|
||||||
}
|
|
||||||
} else if opts.useDirectWrite(userOverride) {
|
|
||||||
_ = os.Remove(userOverride)
|
|
||||||
} else {
|
|
||||||
_ = privesc.Run(context.Background(), opts.sudoPassword, "rm", "-f", userOverride)
|
|
||||||
}
|
|
||||||
|
|
||||||
logFunc(fmt.Sprintf("✓ Synced per-user greeter cache for %s", username))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func localizeSessionWallpapers(session map[string]any, userDir string, opts userSlotSyncOpts) error {
|
|
||||||
stringKeys := []struct {
|
|
||||||
key string
|
|
||||||
prefix string
|
|
||||||
}{
|
|
||||||
{"wallpaperPath", "wallpaper"},
|
|
||||||
{"wallpaperPathLight", "wallpaper-light"},
|
|
||||||
{"wallpaperPathDark", "wallpaper-dark"},
|
|
||||||
}
|
|
||||||
for _, item := range stringKeys {
|
|
||||||
if err := localizeWallpaperStringField(session, item.key, userDir, item.prefix, opts); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mapKeys := []struct {
|
|
||||||
key string
|
|
||||||
prefix string
|
|
||||||
}{
|
|
||||||
{"monitorWallpapers", "wallpaper-monitor"},
|
|
||||||
{"monitorWallpapersLight", "wallpaper-monitor-light"},
|
|
||||||
{"monitorWallpapersDark", "wallpaper-monitor-dark"},
|
|
||||||
}
|
|
||||||
for _, item := range mapKeys {
|
|
||||||
if err := localizeWallpaperMapField(session, item.key, userDir, item.prefix, opts); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func localizeWallpaperStringField(session map[string]any, key, userDir, prefix string, opts userSlotSyncOpts) error {
|
|
||||||
raw, ok := session[key]
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
path, ok := raw.(string)
|
|
||||||
if !ok || strings.TrimSpace(path) == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
dest, err := copyWallpaperIntoUserCache(path, userDir, prefix, opts)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if dest != "" {
|
|
||||||
session[key] = dest
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func localizeWallpaperMapField(session map[string]any, key, userDir, prefix string, opts userSlotSyncOpts) error {
|
|
||||||
raw, ok := session[key]
|
|
||||||
if !ok || raw == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
values, ok := raw.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for monitor, rawPath := range values {
|
|
||||||
path, ok := rawPath.(string)
|
|
||||||
if !ok || strings.TrimSpace(path) == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
safeMonitor := monitorWallpaperSanitizer.ReplaceAllString(monitor, "-")
|
|
||||||
dest, err := copyWallpaperIntoUserCache(path, userDir, prefix+"-"+safeMonitor, opts)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if dest != "" {
|
|
||||||
values[monitor] = dest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyWallpaperIntoUserCache(srcPath, userDir, prefix string, opts userSlotSyncOpts) (string, error) {
|
|
||||||
if strings.TrimSpace(srcPath) == "" {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
st, err := os.Stat(srcPath)
|
|
||||||
if err != nil || st.IsDir() {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
ext := filepath.Ext(srcPath)
|
|
||||||
if ext == "" {
|
|
||||||
ext = ".jpg"
|
|
||||||
}
|
|
||||||
dest := filepath.Join(userDir, prefix+ext)
|
|
||||||
if err := copyFileWithPrivesc(srcPath, dest, opts); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return dest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyFileWithPrivesc(src, dest string, opts userSlotSyncOpts) error {
|
|
||||||
if opts.useDirectWrite(dest) {
|
|
||||||
if err := os.MkdirAll(filepath.Dir(dest), 0o770); err != nil {
|
|
||||||
return fmt.Errorf("failed to create parent dir for %s: %w", dest, err)
|
|
||||||
}
|
|
||||||
data, err := os.ReadFile(src)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read %s: %w", src, err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(dest, data, 0o644); err != nil {
|
|
||||||
return fmt.Errorf("failed to write %s: %w", dest, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isGreeterCachePath(dest) {
|
|
||||||
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
|
|
||||||
return fmt.Errorf("failed to create parent dir for %s: %w", dest, err)
|
|
||||||
}
|
|
||||||
data, err := os.ReadFile(src)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read %s: %w", src, err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(dest, data, 0o644); err != nil {
|
|
||||||
return fmt.Errorf("failed to write %s: %w", dest, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = privesc.Run(context.Background(), opts.sudoPassword, "rm", "-f", dest)
|
|
||||||
if err := privesc.Run(context.Background(), opts.sudoPassword, "cp", src, dest); err != nil {
|
|
||||||
return fmt.Errorf("failed to copy %s to %s: %w", src, dest, err)
|
|
||||||
}
|
|
||||||
return setGreeterCacheFileOwnership(dest, opts.sudoPassword)
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeFileWithPrivesc(path string, data []byte, opts userSlotSyncOpts) error {
|
|
||||||
if opts.useDirectWrite(path) {
|
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0o770); err != nil {
|
|
||||||
return fmt.Errorf("failed to create parent dir for %s: %w", path, err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(path, data, 0o644); err != nil {
|
|
||||||
return fmt.Errorf("failed to write %s: %w", path, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isGreeterCachePath(path) {
|
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
||||||
return fmt.Errorf("failed to create parent dir for %s: %w", path, err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(path, data, 0o644); err != nil {
|
|
||||||
return fmt.Errorf("failed to write %s: %w", path, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tmp, err := os.CreateTemp("", "dms-greeter-user-cache-*")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create temp file for %s: %w", path, err)
|
|
||||||
}
|
|
||||||
tmpPath := tmp.Name()
|
|
||||||
if _, err := tmp.Write(data); err != nil {
|
|
||||||
_ = tmp.Close()
|
|
||||||
_ = os.Remove(tmpPath)
|
|
||||||
return fmt.Errorf("failed to write temp file for %s: %w", path, err)
|
|
||||||
}
|
|
||||||
if err := tmp.Close(); err != nil {
|
|
||||||
_ = os.Remove(tmpPath)
|
|
||||||
return fmt.Errorf("failed to close temp file for %s: %w", path, err)
|
|
||||||
}
|
|
||||||
defer os.Remove(tmpPath)
|
|
||||||
|
|
||||||
_ = privesc.Run(context.Background(), opts.sudoPassword, "rm", "-f", path)
|
|
||||||
if err := privesc.Run(context.Background(), opts.sudoPassword, "cp", tmpPath, path); err != nil {
|
|
||||||
return fmt.Errorf("failed to install %s: %w", path, err)
|
|
||||||
}
|
|
||||||
return setGreeterCacheFileOwnership(path, opts.sudoPassword)
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveUserProfileImageSource(homeDir string) string {
|
|
||||||
candidates := []string{
|
|
||||||
filepath.Join(homeDir, ".face"),
|
|
||||||
filepath.Join(homeDir, ".face.icon"),
|
|
||||||
}
|
|
||||||
if homeDir != "" {
|
|
||||||
username := filepath.Base(homeDir)
|
|
||||||
if username != "" && username != "." && username != string(filepath.Separator) {
|
|
||||||
candidates = append([]string{filepath.Join("/var/lib/AccountsService/icons", username)}, candidates...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, src := range candidates {
|
|
||||||
st, err := os.Stat(src)
|
|
||||||
if err == nil && !st.IsDir() && st.Size() > 0 {
|
|
||||||
return src
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncUserProfileImage(homeDir, userDir string, opts userSlotSyncOpts) error {
|
|
||||||
for _, name := range []string{"profile.jpg", "profile.jpeg", "profile.png", "profile.webp"} {
|
|
||||||
path := filepath.Join(userDir, name)
|
|
||||||
if opts.useDirectWrite(path) {
|
|
||||||
_ = os.Remove(path)
|
|
||||||
} else {
|
|
||||||
_ = privesc.Run(context.Background(), opts.sudoPassword, "rm", "-f", path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
src := resolveUserProfileImageSource(homeDir)
|
|
||||||
if src == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ext := filepath.Ext(src)
|
|
||||||
if ext == "" {
|
|
||||||
ext = ".jpg"
|
|
||||||
}
|
|
||||||
dest := filepath.Join(userDir, "profile"+ext)
|
|
||||||
if err := copyFileWithPrivesc(src, dest, opts); err != nil {
|
|
||||||
return fmt.Errorf("failed to copy profile image for user cache slot: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package greeter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestUserGreeterCacheDir(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
got := userGreeterCacheDir("/var/cache/dms-greeter", "alice")
|
|
||||||
want := filepath.Join("/var/cache/dms-greeter", "users", "alice")
|
|
||||||
if got != want {
|
|
||||||
t.Fatalf("userGreeterCacheDir() = %q, want %q", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveUserProfileImageSource(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
homeDir := t.TempDir()
|
|
||||||
facePath := filepath.Join(homeDir, ".face")
|
|
||||||
writeTestFile(t, facePath, "face")
|
|
||||||
|
|
||||||
got := resolveUserProfileImageSource(homeDir)
|
|
||||||
if got != facePath {
|
|
||||||
t.Fatalf("resolveUserProfileImageSource() = %q, want %q", got, facePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIsUserOwnedGreeterCacheSlot(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
slot := filepath.Join(GreeterCacheDir, "users", "alice", "settings.json")
|
|
||||||
if !isUserOwnedGreeterCacheSlot(slot, "alice") {
|
|
||||||
t.Fatalf("expected alice to own %q", slot)
|
|
||||||
}
|
|
||||||
if isUserOwnedGreeterCacheSlot(slot, "bob") {
|
|
||||||
t.Fatalf("expected bob not to own alice slot")
|
|
||||||
}
|
|
||||||
if isUserOwnedGreeterCacheSlot(filepath.Join(GreeterCacheDir, "settings.json"), "alice") {
|
|
||||||
t.Fatalf("expected root cache file not to be a user slot")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLocalizeSessionWallpapers(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
homeDir := t.TempDir()
|
|
||||||
userDir := filepath.Join(homeDir, "users", "alice")
|
|
||||||
wallpaperPath := filepath.Join(homeDir, "wall.jpg")
|
|
||||||
writeTestFile(t, wallpaperPath, "wallpaper")
|
|
||||||
|
|
||||||
session := map[string]any{
|
|
||||||
"wallpaperPath": wallpaperPath,
|
|
||||||
"monitorWallpapers": map[string]any{
|
|
||||||
"DP-1": wallpaperPath,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := localizeSessionWallpapers(session, userDir, userSlotSyncOpts{}); err != nil {
|
|
||||||
t.Fatalf("localizeSessionWallpapers returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
gotPath, ok := session["wallpaperPath"].(string)
|
|
||||||
if !ok || gotPath == "" {
|
|
||||||
t.Fatalf("expected localized wallpaperPath, got %#v", session["wallpaperPath"])
|
|
||||||
}
|
|
||||||
if gotPath == wallpaperPath {
|
|
||||||
t.Fatalf("expected copied wallpaper path, still points to source")
|
|
||||||
}
|
|
||||||
|
|
||||||
monitorMap, ok := session["monitorWallpapers"].(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected monitorWallpapers map")
|
|
||||||
}
|
|
||||||
monitorPath, ok := monitorMap["DP-1"].(string)
|
|
||||||
if !ok || monitorPath == "" || monitorPath == wallpaperPath {
|
|
||||||
t.Fatalf("expected localized monitor wallpaper, got %#v", monitorMap["DP-1"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrConfirmationRequired is returned when --yes is not set and the user
|
// ErrConfirmationRequired is returned when --yes is not set and the user
|
||||||
@@ -223,16 +223,12 @@ func (r *Runner) Run() error {
|
|||||||
|
|
||||||
// 9. Greeter setup (if dms-greeter was included)
|
// 9. Greeter setup (if dms-greeter was included)
|
||||||
if !disabledItems["dms-greeter"] && r.depExists(dependencies, "dms-greeter") {
|
if !disabledItems["dms-greeter"] && r.depExists(dependencies, "dms-greeter") {
|
||||||
compositorName := "niri"
|
|
||||||
if wm == deps.WindowManagerHyprland {
|
|
||||||
compositorName = "Hyprland"
|
|
||||||
}
|
|
||||||
fmt.Fprintln(os.Stdout, "Configuring DMS greeter...")
|
fmt.Fprintln(os.Stdout, "Configuring DMS greeter...")
|
||||||
logFunc := func(line string) {
|
logFunc := func(line string) {
|
||||||
r.log(line)
|
r.log(line)
|
||||||
fmt.Fprintf(os.Stdout, " greeter: %s\n", line)
|
fmt.Fprintf(os.Stdout, " greeter: %s\n", line)
|
||||||
}
|
}
|
||||||
if err := greeter.AutoSetupGreeter(compositorName, sudoPassword, logFunc); err != nil {
|
if err := utils.RunDmsGreeterInstall(sudoPassword, logFunc); err != nil {
|
||||||
// Non-fatal, matching TUI behavior
|
// Non-fatal, matching TUI behavior
|
||||||
fmt.Fprintf(os.Stderr, "Warning: greeter setup issue (non-fatal): %v\n", err)
|
fmt.Fprintf(os.Stderr, "Warning: greeter setup issue (non-fatal): %v\n", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,30 +5,20 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
GreeterPamManagedBlockStart = "# BEGIN DMS GREETER AUTH (managed by dms greeter sync)"
|
|
||||||
GreeterPamManagedBlockEnd = "# END DMS GREETER AUTH"
|
|
||||||
|
|
||||||
LockscreenPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN AUTH (managed by dms greeter sync)"
|
LockscreenPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN AUTH (managed by dms greeter sync)"
|
||||||
LockscreenPamManagedBlockEnd = "# END DMS LOCKSCREEN AUTH"
|
LockscreenPamManagedBlockEnd = "# END DMS LOCKSCREEN AUTH"
|
||||||
|
|
||||||
LockscreenU2FPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN U2F AUTH (managed by dms auth sync)"
|
LockscreenU2FPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN U2F AUTH (managed by dms auth sync)"
|
||||||
LockscreenU2FPamManagedBlockEnd = "# END DMS LOCKSCREEN U2F AUTH"
|
LockscreenU2FPamManagedBlockEnd = "# END DMS LOCKSCREEN U2F AUTH"
|
||||||
|
|
||||||
legacyGreeterPamFprintComment = "# DMS greeter fingerprint"
|
|
||||||
legacyGreeterPamU2FComment = "# DMS greeter U2F"
|
|
||||||
|
|
||||||
GreetdPamPath = "/etc/pam.d/greetd"
|
|
||||||
DankshellPamPath = "/etc/pam.d/dankshell"
|
DankshellPamPath = "/etc/pam.d/dankshell"
|
||||||
DankshellU2FPamPath = "/etc/pam.d/dankshell-u2f"
|
DankshellU2FPamPath = "/etc/pam.d/dankshell-u2f"
|
||||||
)
|
)
|
||||||
@@ -76,19 +66,14 @@ var includedPamAuthFiles = []string{
|
|||||||
type AuthSettings struct {
|
type AuthSettings struct {
|
||||||
EnableFprint bool `json:"enableFprint"`
|
EnableFprint bool `json:"enableFprint"`
|
||||||
EnableU2f bool `json:"enableU2f"`
|
EnableU2f bool `json:"enableU2f"`
|
||||||
GreeterEnableFprint bool `json:"greeterEnableFprint"`
|
|
||||||
GreeterEnableU2f bool `json:"greeterEnableU2f"`
|
|
||||||
GreeterPamExternallyManaged bool `json:"greeterPamExternallyManaged"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SyncAuthOptions struct {
|
type SyncAuthOptions struct {
|
||||||
HomeDir string
|
HomeDir string
|
||||||
ForceGreeterAuth bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type syncDeps struct {
|
type syncDeps struct {
|
||||||
pamDir string
|
pamDir string
|
||||||
greetdPath string
|
|
||||||
dankshellPath string
|
dankshellPath string
|
||||||
dankshellU2fPath string
|
dankshellU2fPath string
|
||||||
isNixOS func() bool
|
isNixOS func() bool
|
||||||
@@ -97,8 +82,6 @@ type syncDeps struct {
|
|||||||
createTemp func(string, string) (*os.File, error)
|
createTemp func(string, string) (*os.File, error)
|
||||||
removeFile func(string) error
|
removeFile func(string) error
|
||||||
runSudoCmd func(string, string, ...string) error
|
runSudoCmd func(string, string, ...string) error
|
||||||
pamModuleExists func(string) bool
|
|
||||||
fingerprintAvailableForCurrentUser func() bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type lockscreenPamIncludeDirective struct {
|
type lockscreenPamIncludeDirective struct {
|
||||||
@@ -154,7 +137,6 @@ func (r lockscreenPamResolver) locate(target string) (string, error) {
|
|||||||
func defaultSyncDeps() syncDeps {
|
func defaultSyncDeps() syncDeps {
|
||||||
return syncDeps{
|
return syncDeps{
|
||||||
pamDir: "/etc/pam.d",
|
pamDir: "/etc/pam.d",
|
||||||
greetdPath: GreetdPamPath,
|
|
||||||
dankshellPath: DankshellPamPath,
|
dankshellPath: DankshellPamPath,
|
||||||
dankshellU2fPath: DankshellU2FPamPath,
|
dankshellU2fPath: DankshellU2FPamPath,
|
||||||
isNixOS: IsNixOS,
|
isNixOS: IsNixOS,
|
||||||
@@ -165,8 +147,6 @@ func defaultSyncDeps() syncDeps {
|
|||||||
runSudoCmd: func(password, command string, args ...string) error {
|
runSudoCmd: func(password, command string, args ...string) error {
|
||||||
return privesc.Run(context.Background(), password, append([]string{command}, args...)...)
|
return privesc.Run(context.Background(), password, append([]string{command}, args...)...)
|
||||||
},
|
},
|
||||||
pamModuleExists: pamModuleExists,
|
|
||||||
fingerprintAvailableForCurrentUser: FingerprintAuthAvailableForCurrentUser,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,22 +175,10 @@ func ReadAuthSettings(homeDir string) (AuthSettings, error) {
|
|||||||
return settings, nil
|
return settings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReadGreeterAuthToggles(homeDir string) (enableFprint bool, enableU2f bool, err error) {
|
|
||||||
settings, err := ReadAuthSettings(homeDir)
|
|
||||||
if err != nil {
|
|
||||||
return false, false, err
|
|
||||||
}
|
|
||||||
return settings.GreeterEnableFprint, settings.GreeterEnableU2f, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func SyncAuthConfig(logFunc func(string), sudoPassword string, options SyncAuthOptions) error {
|
func SyncAuthConfig(logFunc func(string), sudoPassword string, options SyncAuthOptions) error {
|
||||||
return syncAuthConfigWithDeps(logFunc, sudoPassword, options, defaultSyncDeps())
|
return syncAuthConfigWithDeps(logFunc, sudoPassword, options, defaultSyncDeps())
|
||||||
}
|
}
|
||||||
|
|
||||||
func RemoveManagedGreeterPamBlock(logFunc func(string), sudoPassword string) error {
|
|
||||||
return removeManagedGreeterPamBlockWithDeps(logFunc, sudoPassword, defaultSyncDeps())
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncAuthConfigWithDeps(logFunc func(string), sudoPassword string, options SyncAuthOptions, deps syncDeps) error {
|
func syncAuthConfigWithDeps(logFunc func(string), sudoPassword string, options SyncAuthOptions, deps syncDeps) error {
|
||||||
homeDir := strings.TrimSpace(options.HomeDir)
|
homeDir := strings.TrimSpace(options.HomeDir)
|
||||||
if homeDir == "" {
|
if homeDir == "" {
|
||||||
@@ -233,98 +201,8 @@ func syncAuthConfigWithDeps(logFunc func(string), sudoPassword string, options S
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := deps.stat(deps.greetdPath); err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
logFunc("ℹ /etc/pam.d/greetd not found. Skipping greeter PAM sync.")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return fmt.Errorf("failed to inspect %s: %w", deps.greetdPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if settings.GreeterPamExternallyManaged {
|
|
||||||
if err := removeManagedGreeterPamBlockWithDeps(logFunc, sudoPassword, deps); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
logFunc("ℹ /etc/pam.d/greetd is externally managed. Skipping DMS greeter PAM sync.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := syncGreeterPamConfigWithDeps(logFunc, sudoPassword, settings, options.ForceGreeterAuth, deps); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeManagedGreeterPamBlockWithDeps(logFunc func(string), sudoPassword string, deps syncDeps) error {
|
|
||||||
if deps.isNixOS() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := deps.readFile(deps.greetdPath)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("failed to read %s: %w", deps.greetdPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
originalContent := string(data)
|
|
||||||
stripped, removed := stripManagedGreeterPamBlock(originalContent)
|
|
||||||
strippedAgain, removedLegacy := stripLegacyGreeterPamLines(stripped)
|
|
||||||
if !removed && !removedLegacy {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := writeManagedPamFile(strippedAgain, deps.greetdPath, sudoPassword, deps); err != nil {
|
|
||||||
return fmt.Errorf("failed to write %s: %w", deps.greetdPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logFunc("✓ Removed DMS managed PAM block from " + deps.greetdPath)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
switch trimmed {
|
|
||||||
case GreeterPamManagedBlockStart:
|
|
||||||
managed = true
|
|
||||||
inManaged = true
|
|
||||||
continue
|
|
||||||
case GreeterPamManagedBlockEnd:
|
|
||||||
inManaged = false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) {
|
|
||||||
legacy = true
|
|
||||||
}
|
|
||||||
if !inManaged {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.Contains(trimmed, "pam_fprintd") {
|
|
||||||
fingerprint = true
|
|
||||||
}
|
|
||||||
if strings.Contains(trimmed, "pam_u2f") {
|
|
||||||
u2f = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return managed, fingerprint, u2f, legacy
|
|
||||||
}
|
|
||||||
|
|
||||||
func StripManagedGreeterPamContent(pamText string) (string, bool) {
|
|
||||||
stripped, removed := stripManagedGreeterPamBlock(pamText)
|
|
||||||
stripped, removedLegacy := stripLegacyGreeterPamLines(stripped)
|
|
||||||
return stripped, removed || removedLegacy
|
|
||||||
}
|
|
||||||
|
|
||||||
func PamTextIncludesFile(pamText, filename string) bool {
|
func PamTextIncludesFile(pamText, filename string) bool {
|
||||||
lines := strings.Split(pamText, "\n")
|
lines := strings.Split(pamText, "\n")
|
||||||
@@ -1034,186 +912,6 @@ func syncLockscreenU2FPamConfigWithDeps(logFunc func(string), sudoPassword strin
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func stripManagedGreeterPamBlock(content string) (string, bool) {
|
|
||||||
lines := strings.Split(content, "\n")
|
|
||||||
filtered := make([]string, 0, len(lines))
|
|
||||||
inManagedBlock := false
|
|
||||||
removed := false
|
|
||||||
|
|
||||||
for _, line := range lines {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
if trimmed == GreeterPamManagedBlockStart {
|
|
||||||
inManagedBlock = true
|
|
||||||
removed = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if trimmed == GreeterPamManagedBlockEnd {
|
|
||||||
inManagedBlock = false
|
|
||||||
removed = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if inManagedBlock {
|
|
||||||
removed = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
filtered = append(filtered, line)
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(filtered, "\n"), removed
|
|
||||||
}
|
|
||||||
|
|
||||||
func stripLegacyGreeterPamLines(content string) (string, bool) {
|
|
||||||
lines := strings.Split(content, "\n")
|
|
||||||
filtered := make([]string, 0, len(lines))
|
|
||||||
removed := false
|
|
||||||
|
|
||||||
for i := 0; i < len(lines); i++ {
|
|
||||||
trimmed := strings.TrimSpace(lines[i])
|
|
||||||
if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) {
|
|
||||||
removed = true
|
|
||||||
if i+1 < len(lines) {
|
|
||||||
nextLine := strings.TrimSpace(lines[i+1])
|
|
||||||
if strings.HasPrefix(nextLine, "auth") &&
|
|
||||||
(strings.Contains(nextLine, "pam_fprintd") || strings.Contains(nextLine, "pam_u2f")) {
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
filtered = append(filtered, lines[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(filtered, "\n"), removed
|
|
||||||
}
|
|
||||||
|
|
||||||
func insertManagedGreeterPamBlock(content string, blockLines []string, greetdPamPath string) (string, error) {
|
|
||||||
lines := strings.Split(content, "\n")
|
|
||||||
for i, line := range lines {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
if trimmed != "" && !strings.HasPrefix(trimmed, "#") && strings.HasPrefix(trimmed, "auth") {
|
|
||||||
block := strings.Join(blockLines, "\n")
|
|
||||||
prefix := strings.Join(lines[:i], "\n")
|
|
||||||
suffix := strings.Join(lines[i:], "\n")
|
|
||||||
switch {
|
|
||||||
case prefix == "":
|
|
||||||
return block + "\n" + suffix, nil
|
|
||||||
case suffix == "":
|
|
||||||
return prefix + "\n" + block, nil
|
|
||||||
default:
|
|
||||||
return prefix + "\n" + block + "\n" + suffix, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("no auth directive found in %s", greetdPamPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncGreeterPamConfigWithDeps(logFunc func(string), sudoPassword string, settings AuthSettings, forceAuth bool, deps syncDeps) error {
|
|
||||||
var wantFprint, wantU2f bool
|
|
||||||
fprintToggleEnabled := forceAuth
|
|
||||||
u2fToggleEnabled := forceAuth
|
|
||||||
if forceAuth {
|
|
||||||
wantFprint = deps.pamModuleExists("pam_fprintd.so")
|
|
||||||
wantU2f = deps.pamModuleExists("pam_u2f.so")
|
|
||||||
} else {
|
|
||||||
fprintToggleEnabled = settings.GreeterEnableFprint
|
|
||||||
u2fToggleEnabled = settings.GreeterEnableU2f
|
|
||||||
fprintModule := deps.pamModuleExists("pam_fprintd.so")
|
|
||||||
u2fModule := deps.pamModuleExists("pam_u2f.so")
|
|
||||||
wantFprint = settings.GreeterEnableFprint && fprintModule
|
|
||||||
wantU2f = settings.GreeterEnableU2f && u2fModule
|
|
||||||
if settings.GreeterEnableFprint && !fprintModule {
|
|
||||||
logFunc("⚠ Warning: greeter fingerprint toggle is enabled, but pam_fprintd.so was not found.")
|
|
||||||
}
|
|
||||||
if settings.GreeterEnableU2f && !u2fModule {
|
|
||||||
logFunc("⚠ Warning: greeter security key toggle is enabled, but pam_u2f.so was not found.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if deps.isNixOS() {
|
|
||||||
logFunc("ℹ NixOS detected: PAM config is managed by NixOS modules. Skipping DMS PAM block write.")
|
|
||||||
logFunc(" Configure fingerprint/U2F auth via your greetd NixOS module options (e.g. security.pam.services.greetd).")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
pamData, err := deps.readFile(deps.greetdPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read %s: %w", deps.greetdPath, err)
|
|
||||||
}
|
|
||||||
originalContent := string(pamData)
|
|
||||||
content, _ := stripManagedGreeterPamBlock(originalContent)
|
|
||||||
content, _ = stripLegacyGreeterPamLines(content)
|
|
||||||
|
|
||||||
includedFprintFile := detectIncludedPamModule(content, "pam_fprintd.so", deps)
|
|
||||||
includedU2fFile := detectIncludedPamModule(content, "pam_u2f.so", deps)
|
|
||||||
fprintAvailableForCurrentUser := deps.fingerprintAvailableForCurrentUser()
|
|
||||||
if wantFprint && includedFprintFile != "" {
|
|
||||||
logFunc("⚠ pam_fprintd already present in included " + includedFprintFile + " (managed by authselect/pam-auth-update). Skipping DMS fprint block to avoid double-fingerprint auth.")
|
|
||||||
wantFprint = false
|
|
||||||
}
|
|
||||||
if wantU2f && includedU2fFile != "" {
|
|
||||||
logFunc("⚠ pam_u2f already present in included " + includedU2fFile + " (managed by authselect/pam-auth-update). Skipping DMS U2F block to avoid double security-key auth.")
|
|
||||||
wantU2f = false
|
|
||||||
}
|
|
||||||
if !wantFprint && includedFprintFile != "" {
|
|
||||||
if fprintToggleEnabled {
|
|
||||||
logFunc("ℹ Fingerprint auth is still enabled via included " + includedFprintFile + ".")
|
|
||||||
if fprintAvailableForCurrentUser {
|
|
||||||
logFunc(" DMS toggle is enabled, and effective auth is provided by the included PAM stack.")
|
|
||||||
} else {
|
|
||||||
logFunc(" No enrolled fingerprints detected for the current user; password auth remains the effective path.")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if fprintAvailableForCurrentUser {
|
|
||||||
logFunc("ℹ Fingerprint auth is active via included " + includedFprintFile + " while DMS fingerprint toggle is off.")
|
|
||||||
logFunc(" Password login will work but may be delayed while the fingerprint module runs first.")
|
|
||||||
logFunc(" To eliminate the delay, " + pamManagerHintForCurrentDistro())
|
|
||||||
} else {
|
|
||||||
logFunc("ℹ pam_fprintd is present via included " + includedFprintFile + ", but no enrolled fingerprints were detected for the current user.")
|
|
||||||
logFunc(" Password auth remains the effective login path.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !wantU2f && includedU2fFile != "" {
|
|
||||||
if u2fToggleEnabled {
|
|
||||||
logFunc("ℹ Security-key auth is still enabled via included " + includedU2fFile + ".")
|
|
||||||
logFunc(" DMS toggle is enabled, but effective auth is provided by the included PAM stack.")
|
|
||||||
} else {
|
|
||||||
logFunc("⚠ Security-key auth is active via included " + includedU2fFile + " while DMS security-key toggle is off.")
|
|
||||||
logFunc(" " + pamManagerHintForCurrentDistro())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if wantFprint || wantU2f {
|
|
||||||
blockLines := []string{GreeterPamManagedBlockStart}
|
|
||||||
if wantFprint {
|
|
||||||
blockLines = append(blockLines, "auth sufficient pam_fprintd.so max-tries=2 timeout=10")
|
|
||||||
}
|
|
||||||
if wantU2f {
|
|
||||||
blockLines = append(blockLines, "auth sufficient pam_u2f.so cue nouserok timeout=10")
|
|
||||||
}
|
|
||||||
blockLines = append(blockLines, GreeterPamManagedBlockEnd)
|
|
||||||
|
|
||||||
content, err = insertManagedGreeterPamBlock(content, blockLines, deps.greetdPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if content == originalContent {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := writeManagedPamFile(content, deps.greetdPath, sudoPassword, deps); err != nil {
|
|
||||||
return fmt.Errorf("failed to install updated PAM config at %s: %w", deps.greetdPath, err)
|
|
||||||
}
|
|
||||||
if wantFprint || wantU2f {
|
|
||||||
logFunc("✓ Configured greetd PAM for fingerprint/U2F")
|
|
||||||
} else {
|
|
||||||
logFunc("✓ Cleared DMS-managed greeter PAM auth block")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeManagedPamFile(content string, destPath string, sudoPassword string, deps syncDeps) error {
|
func writeManagedPamFile(content string, destPath string, sudoPassword string, deps syncDeps) error {
|
||||||
tmpFile, err := deps.createTemp("", "dms-pam-*.conf")
|
tmpFile, err := deps.createTemp("", "dms-pam-*.conf")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1240,26 +938,6 @@ func writeManagedPamFile(content string, destPath string, sudoPassword string, d
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func pamManagerHintForCurrentDistro() string {
|
|
||||||
osInfo, err := distros.GetOSInfo()
|
|
||||||
if err != nil {
|
|
||||||
return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
|
|
||||||
}
|
|
||||||
config, exists := distros.Registry[osInfo.Distribution.ID]
|
|
||||||
if !exists {
|
|
||||||
return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
|
|
||||||
}
|
|
||||||
|
|
||||||
switch config.Family {
|
|
||||||
case distros.FamilyFedora:
|
|
||||||
return "Disable it in authselect to force password-only greeter login."
|
|
||||||
case distros.FamilyDebian, distros.FamilyUbuntu:
|
|
||||||
return "Disable it in pam-auth-update to force password-only greeter login."
|
|
||||||
default:
|
|
||||||
return "Disable it in your distro PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func pamModuleExists(module string) bool {
|
func pamModuleExists(module string) bool {
|
||||||
for _, libDir := range []string{
|
for _, libDir := range []string{
|
||||||
"/usr/lib64/security",
|
"/usr/lib64/security",
|
||||||
@@ -1282,60 +960,3 @@ func pamModuleExists(module string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasEnrolledFingerprintOutput(output string) bool {
|
|
||||||
lower := strings.ToLower(output)
|
|
||||||
if strings.Contains(lower, "no fingers enrolled") ||
|
|
||||||
strings.Contains(lower, "no fingerprints enrolled") ||
|
|
||||||
strings.Contains(lower, "no prints enrolled") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if strings.Contains(lower, "has fingers enrolled") ||
|
|
||||||
strings.Contains(lower, "has fingerprints enrolled") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
for _, line := range strings.Split(lower, "\n") {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
if strings.HasPrefix(trimmed, "finger:") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(trimmed, "- ") && strings.Contains(trimmed, "finger") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func FingerprintAuthAvailableForCurrentUser() bool {
|
|
||||||
username := strings.TrimSpace(os.Getenv("SUDO_USER"))
|
|
||||||
if username == "" {
|
|
||||||
username = strings.TrimSpace(os.Getenv("USER"))
|
|
||||||
}
|
|
||||||
if username == "" {
|
|
||||||
out, err := exec.Command("id", "-un").Output()
|
|
||||||
if err == nil {
|
|
||||||
username = strings.TrimSpace(string(out))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fingerprintAuthAvailableForUser(username)
|
|
||||||
}
|
|
||||||
|
|
||||||
func fingerprintAuthAvailableForUser(username string) bool {
|
|
||||||
username = strings.TrimSpace(username)
|
|
||||||
if username == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !pamModuleExists("pam_fprintd.so") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if _, err := exec.LookPath("fprintd-list"); err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
out, err := exec.CommandContext(ctx, "fprintd-list", username).CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return hasEnrolledFingerprintOutput(string(out))
|
|
||||||
}
|
|
||||||
|
|||||||
+94
-271
@@ -18,42 +18,6 @@ func writeTestFile(t *testing.T, path string, content string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type pamTestEnv struct {
|
|
||||||
pamDir string
|
|
||||||
greetdPath string
|
|
||||||
dankshellPath string
|
|
||||||
dankshellU2fPath string
|
|
||||||
tmpDir string
|
|
||||||
homeDir string
|
|
||||||
availableModules map[string]bool
|
|
||||||
fingerprintAvailable bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPamTestEnv(t *testing.T) *pamTestEnv {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
root := t.TempDir()
|
|
||||||
pamDir := filepath.Join(root, "pam.d")
|
|
||||||
tmpDir := filepath.Join(root, "tmp")
|
|
||||||
homeDir := filepath.Join(root, "home")
|
|
||||||
|
|
||||||
for _, dir := range []string{pamDir, tmpDir, homeDir} {
|
|
||||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
||||||
t.Fatalf("failed to create %s: %v", dir, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &pamTestEnv{
|
|
||||||
pamDir: pamDir,
|
|
||||||
greetdPath: filepath.Join(pamDir, "greetd"),
|
|
||||||
dankshellPath: filepath.Join(pamDir, "dankshell"),
|
|
||||||
dankshellU2fPath: filepath.Join(pamDir, "dankshell-u2f"),
|
|
||||||
tmpDir: tmpDir,
|
|
||||||
homeDir: homeDir,
|
|
||||||
availableModules: map[string]bool{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *pamTestEnv) writePamFile(t *testing.T, name string, content string) {
|
func (e *pamTestEnv) writePamFile(t *testing.T, name string, content string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
writeTestFile(t, filepath.Join(e.pamDir, name), content)
|
writeTestFile(t, filepath.Join(e.pamDir, name), content)
|
||||||
@@ -64,59 +28,6 @@ func (e *pamTestEnv) writeSettings(t *testing.T, content string) {
|
|||||||
writeTestFile(t, filepath.Join(e.homeDir, ".config", "DankMaterialShell", "settings.json"), content)
|
writeTestFile(t, filepath.Join(e.homeDir, ".config", "DankMaterialShell", "settings.json"), content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *pamTestEnv) deps(isNixOS bool) syncDeps {
|
|
||||||
return syncDeps{
|
|
||||||
pamDir: e.pamDir,
|
|
||||||
greetdPath: e.greetdPath,
|
|
||||||
dankshellPath: e.dankshellPath,
|
|
||||||
dankshellU2fPath: e.dankshellU2fPath,
|
|
||||||
isNixOS: func() bool { return isNixOS },
|
|
||||||
readFile: os.ReadFile,
|
|
||||||
stat: os.Stat,
|
|
||||||
createTemp: func(_ string, pattern string) (*os.File, error) {
|
|
||||||
return os.CreateTemp(e.tmpDir, pattern)
|
|
||||||
},
|
|
||||||
removeFile: os.Remove,
|
|
||||||
runSudoCmd: func(_ string, command string, args ...string) error {
|
|
||||||
switch command {
|
|
||||||
case "cp":
|
|
||||||
if len(args) != 2 {
|
|
||||||
return fmt.Errorf("unexpected cp args: %v", args)
|
|
||||||
}
|
|
||||||
data, err := os.ReadFile(args[0])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(filepath.Dir(args[1]), 0o755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.WriteFile(args[1], data, 0o644)
|
|
||||||
case "chmod":
|
|
||||||
if len(args) != 2 {
|
|
||||||
return fmt.Errorf("unexpected chmod args: %v", args)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case "rm":
|
|
||||||
if len(args) != 2 || args[0] != "-f" {
|
|
||||||
return fmt.Errorf("unexpected rm args: %v", args)
|
|
||||||
}
|
|
||||||
if err := os.Remove(args[1]); err != nil && !os.IsNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unexpected sudo command: %s %v", command, args)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
pamModuleExists: func(module string) bool {
|
|
||||||
return e.availableModules[module]
|
|
||||||
},
|
|
||||||
fingerprintAvailableForCurrentUser: func() bool {
|
|
||||||
return e.fingerprintAvailable
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func readFileString(t *testing.T, path string) string {
|
func readFileString(t *testing.T, path string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
@@ -703,98 +614,6 @@ func TestSyncLockscreenU2FPamConfigWithDeps(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSyncGreeterPamConfigWithDeps(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
t.Run("adds managed block for enabled auth modules", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
env := newPamTestEnv(t)
|
|
||||||
env.availableModules["pam_fprintd.so"] = true
|
|
||||||
env.availableModules["pam_u2f.so"] = true
|
|
||||||
env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
|
|
||||||
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so\naccount required pam_unix.so\n")
|
|
||||||
|
|
||||||
settings := AuthSettings{GreeterEnableFprint: true, GreeterEnableU2f: true}
|
|
||||||
if err := syncGreeterPamConfigWithDeps(func(string) {}, "", settings, false, env.deps(false)); err != nil {
|
|
||||||
t.Fatalf("syncGreeterPamConfigWithDeps returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
got := readFileString(t, env.greetdPath)
|
|
||||||
for _, want := range []string{
|
|
||||||
GreeterPamManagedBlockStart,
|
|
||||||
"auth sufficient pam_fprintd.so max-tries=2 timeout=10",
|
|
||||||
"auth sufficient pam_u2f.so cue nouserok timeout=10",
|
|
||||||
GreeterPamManagedBlockEnd,
|
|
||||||
} {
|
|
||||||
if !strings.Contains(got, want) {
|
|
||||||
t.Errorf("missing expected string %q in greetd PAM:\n%s", want, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if strings.Index(got, GreeterPamManagedBlockStart) > strings.Index(got, "auth include system-auth") {
|
|
||||||
t.Fatalf("managed block was not inserted before first auth line:\n%s", got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("avoids duplicate fingerprint when included stack already provides it", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
env := newPamTestEnv(t)
|
|
||||||
env.availableModules["pam_fprintd.so"] = true
|
|
||||||
env.fingerprintAvailable = true
|
|
||||||
original := "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n"
|
|
||||||
env.writePamFile(t, "greetd", original)
|
|
||||||
env.writePamFile(t, "system-auth", "auth sufficient pam_fprintd.so max-tries=1\nauth sufficient pam_unix.so\n")
|
|
||||||
|
|
||||||
settings := AuthSettings{GreeterEnableFprint: true}
|
|
||||||
if err := syncGreeterPamConfigWithDeps(func(string) {}, "", settings, false, env.deps(false)); err != nil {
|
|
||||||
t.Fatalf("syncGreeterPamConfigWithDeps returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
got := readFileString(t, env.greetdPath)
|
|
||||||
if got != original {
|
|
||||||
t.Fatalf("greetd PAM changed despite included pam_fprintd stack\ngot:\n%s\nwant:\n%s", got, original)
|
|
||||||
}
|
|
||||||
if strings.Contains(got, GreeterPamManagedBlockStart) {
|
|
||||||
t.Fatalf("managed block should not be inserted when included stack already has pam_fprintd:\n%s", got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemoveManagedGreeterPamBlockWithDeps(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
env := newPamTestEnv(t)
|
|
||||||
env.writePamFile(t, "greetd", "#%PAM-1.0\n"+
|
|
||||||
legacyGreeterPamFprintComment+"\n"+
|
|
||||||
"auth sufficient pam_fprintd.so max-tries=1\n"+
|
|
||||||
GreeterPamManagedBlockStart+"\n"+
|
|
||||||
"auth sufficient pam_u2f.so cue nouserok timeout=10\n"+
|
|
||||||
GreeterPamManagedBlockEnd+"\n"+
|
|
||||||
"auth include system-auth\n")
|
|
||||||
|
|
||||||
if err := removeManagedGreeterPamBlockWithDeps(func(string) {}, "", env.deps(false)); err != nil {
|
|
||||||
t.Fatalf("removeManagedGreeterPamBlockWithDeps returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
got := readFileString(t, env.greetdPath)
|
|
||||||
if strings.Contains(got, GreeterPamManagedBlockStart) || strings.Contains(got, legacyGreeterPamFprintComment) {
|
|
||||||
t.Fatalf("managed or legacy DMS auth lines remained in greetd PAM:\n%s", got)
|
|
||||||
}
|
|
||||||
if !strings.Contains(got, "auth include system-auth") {
|
|
||||||
t.Fatalf("expected non-DMS greetd auth lines to remain:\n%s", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *pamTestEnv) validateDeps() lockscreenPamValidateDeps {
|
|
||||||
return lockscreenPamValidateDeps{
|
|
||||||
baseDirs: []string{e.pamDir},
|
|
||||||
readFile: os.ReadFile,
|
|
||||||
stat: os.Stat,
|
|
||||||
pamModuleExists: func(module string) bool { return e.availableModules[module] },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestListLockscreenPamServices(t *testing.T) {
|
func TestListLockscreenPamServices(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -1081,10 +900,98 @@ func containsSubstr(items []string, substr string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type pamTestEnv struct {
|
||||||
|
pamDir string
|
||||||
|
dankshellPath string
|
||||||
|
dankshellU2fPath string
|
||||||
|
tmpDir string
|
||||||
|
homeDir string
|
||||||
|
availableModules map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPamTestEnv(t *testing.T) *pamTestEnv {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
root := t.TempDir()
|
||||||
|
pamDir := filepath.Join(root, "pam.d")
|
||||||
|
tmpDir := filepath.Join(root, "tmp")
|
||||||
|
homeDir := filepath.Join(root, "home")
|
||||||
|
|
||||||
|
for _, dir := range []string{pamDir, tmpDir, homeDir} {
|
||||||
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
|
t.Fatalf("failed to create %s: %v", dir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pamTestEnv{
|
||||||
|
pamDir: pamDir,
|
||||||
|
dankshellPath: filepath.Join(pamDir, "dankshell"),
|
||||||
|
dankshellU2fPath: filepath.Join(pamDir, "dankshell-u2f"),
|
||||||
|
tmpDir: tmpDir,
|
||||||
|
homeDir: homeDir,
|
||||||
|
availableModules: map[string]bool{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *pamTestEnv) deps(isNixOS bool) syncDeps {
|
||||||
|
return syncDeps{
|
||||||
|
pamDir: e.pamDir,
|
||||||
|
dankshellPath: e.dankshellPath,
|
||||||
|
dankshellU2fPath: e.dankshellU2fPath,
|
||||||
|
isNixOS: func() bool { return isNixOS },
|
||||||
|
readFile: os.ReadFile,
|
||||||
|
stat: os.Stat,
|
||||||
|
createTemp: func(_ string, pattern string) (*os.File, error) {
|
||||||
|
return os.CreateTemp(e.tmpDir, pattern)
|
||||||
|
},
|
||||||
|
removeFile: os.Remove,
|
||||||
|
runSudoCmd: func(_ string, command string, args ...string) error {
|
||||||
|
switch command {
|
||||||
|
case "cp":
|
||||||
|
if len(args) != 2 {
|
||||||
|
return fmt.Errorf("unexpected cp args: %v", args)
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(args[1]), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(args[1], data, 0o644)
|
||||||
|
case "chmod":
|
||||||
|
if len(args) != 2 {
|
||||||
|
return fmt.Errorf("unexpected chmod args: %v", args)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case "rm":
|
||||||
|
if len(args) != 2 || args[0] != "-f" {
|
||||||
|
return fmt.Errorf("unexpected rm args: %v", args)
|
||||||
|
}
|
||||||
|
if err := os.Remove(args[1]); err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unexpected sudo command: %s %v", command, args)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *pamTestEnv) validateDeps() lockscreenPamValidateDeps {
|
||||||
|
return lockscreenPamValidateDeps{
|
||||||
|
baseDirs: []string{e.pamDir},
|
||||||
|
readFile: os.ReadFile,
|
||||||
|
stat: os.Stat,
|
||||||
|
pamModuleExists: func(module string) bool { return e.availableModules[module] },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSyncAuthConfigWithDeps(t *testing.T) {
|
func TestSyncAuthConfigWithDeps(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
t.Run("creates lockscreen targets and skips greetd when greeter is not installed", func(t *testing.T) {
|
t.Run("creates lockscreen targets", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
env := newPamTestEnv(t)
|
env := newPamTestEnv(t)
|
||||||
@@ -1092,10 +999,7 @@ func TestSyncAuthConfigWithDeps(t *testing.T) {
|
|||||||
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
|
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
|
||||||
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\naccount required pam_access.so\n")
|
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\naccount required pam_access.so\n")
|
||||||
|
|
||||||
var logs []string
|
err := syncAuthConfigWithDeps(func(string) {}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
|
||||||
err := syncAuthConfigWithDeps(func(msg string) {
|
|
||||||
logs = append(logs, msg)
|
|
||||||
}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
|
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1106,105 +1010,24 @@ func TestSyncAuthConfigWithDeps(t *testing.T) {
|
|||||||
if got := readFileString(t, env.dankshellU2fPath); got != buildManagedLockscreenU2FPamContent() {
|
if got := readFileString(t, env.dankshellU2fPath); got != buildManagedLockscreenU2FPamContent() {
|
||||||
t.Fatalf("unexpected dankshell-u2f content:\n%s", got)
|
t.Fatalf("unexpected dankshell-u2f content:\n%s", got)
|
||||||
}
|
}
|
||||||
if len(logs) == 0 || !strings.Contains(logs[len(logs)-1], "greetd not found") {
|
|
||||||
t.Fatalf("expected greetd skip log, got %v", logs)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("separate greeter and lockscreen toggles are respected", func(t *testing.T) {
|
t.Run("removes dankshell-u2f when disabled", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
env := newPamTestEnv(t)
|
env := newPamTestEnv(t)
|
||||||
env.availableModules["pam_fprintd.so"] = true
|
env.writeSettings(t, `{"enableU2f":false}`)
|
||||||
env.writeSettings(t, `{"enableU2f":false,"greeterEnableFprint":true,"greeterEnableU2f":false}`)
|
|
||||||
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
|
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
|
||||||
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\naccount required pam_access.so\n")
|
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\naccount required pam_access.so\n")
|
||||||
env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
|
env.writePamFile(t, "dankshell-u2f", buildManagedLockscreenU2FPamContent())
|
||||||
|
|
||||||
err := syncAuthConfigWithDeps(func(string) {}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
|
err := syncAuthConfigWithDeps(func(string) {}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
|
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dankshell := readFileString(t, env.dankshellPath)
|
|
||||||
if strings.Contains(dankshell, "pam_fprintd") || strings.Contains(dankshell, "pam_u2f") {
|
|
||||||
t.Fatalf("lockscreen PAM should strip fingerprint and U2F modules:\n%s", dankshell)
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) {
|
if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) {
|
||||||
t.Fatalf("expected dankshell-u2f to remain absent when enableU2f is false, stat err = %v", err)
|
t.Fatalf("expected dankshell-u2f to be removed, stat err = %v", err)
|
||||||
}
|
|
||||||
|
|
||||||
greetd := readFileString(t, env.greetdPath)
|
|
||||||
if !strings.Contains(greetd, "auth sufficient pam_fprintd.so max-tries=2 timeout=10") {
|
|
||||||
t.Fatalf("expected greetd PAM to receive fingerprint auth block:\n%s", greetd)
|
|
||||||
}
|
|
||||||
if strings.Contains(greetd, "auth sufficient pam_u2f.so cue nouserok timeout=10") {
|
|
||||||
t.Fatalf("did not expect greetd PAM to receive U2F auth block:\n%s", greetd)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("externally managed greetd is stripped and greeter sync skipped", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
env := newPamTestEnv(t)
|
|
||||||
env.availableModules["pam_fprintd.so"] = true
|
|
||||||
env.writeSettings(t, `{"greeterPamExternallyManaged":true,"greeterEnableFprint":true}`)
|
|
||||||
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
|
|
||||||
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so\naccount required pam_unix.so\n")
|
|
||||||
env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\n"+
|
|
||||||
GreeterPamManagedBlockStart+"\n"+
|
|
||||||
"auth sufficient pam_fprintd.so max-tries=2 timeout=10\n"+
|
|
||||||
GreeterPamManagedBlockEnd+"\n")
|
|
||||||
|
|
||||||
var logs []string
|
|
||||||
err := syncAuthConfigWithDeps(func(msg string) {
|
|
||||||
logs = append(logs, msg)
|
|
||||||
}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
greetd := readFileString(t, env.greetdPath)
|
|
||||||
if strings.Contains(greetd, GreeterPamManagedBlockStart) || strings.Contains(greetd, "pam_fprintd") {
|
|
||||||
t.Fatalf("expected DMS-managed block stripped from externally managed greetd:\n%s", greetd)
|
|
||||||
}
|
|
||||||
if !strings.Contains(greetd, "auth include system-auth") {
|
|
||||||
t.Fatalf("expected non-DMS greetd lines to remain:\n%s", greetd)
|
|
||||||
}
|
|
||||||
if !containsSubstr(logs, "externally managed") {
|
|
||||||
t.Fatalf("expected externally-managed skip log, got %v", logs)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("NixOS remains informational and non-mutating", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
env := newPamTestEnv(t)
|
|
||||||
env.availableModules["pam_fprintd.so"] = true
|
|
||||||
env.availableModules["pam_u2f.so"] = true
|
|
||||||
env.writeSettings(t, `{"enableU2f":true,"greeterEnableFprint":true,"greeterEnableU2f":true}`)
|
|
||||||
originalGreetd := "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n"
|
|
||||||
env.writePamFile(t, "greetd", originalGreetd)
|
|
||||||
|
|
||||||
var logs []string
|
|
||||||
err := syncAuthConfigWithDeps(func(msg string) {
|
|
||||||
logs = append(logs, msg)
|
|
||||||
}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(true))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := os.Stat(env.dankshellPath); !os.IsNotExist(err) {
|
|
||||||
t.Fatalf("expected dankshell to remain absent on NixOS path, stat err = %v", err)
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) {
|
|
||||||
t.Fatalf("expected dankshell-u2f to remain absent on NixOS path, stat err = %v", err)
|
|
||||||
}
|
|
||||||
if got := readFileString(t, env.greetdPath); got != originalGreetd {
|
|
||||||
t.Fatalf("expected greetd PAM to remain unchanged on NixOS path\ngot:\n%s\nwant:\n%s", got, originalGreetd)
|
|
||||||
}
|
|
||||||
if len(logs) < 2 || !strings.Contains(strings.Join(logs, "\n"), "NixOS detected") {
|
|
||||||
t.Fatalf("expected informational NixOS logs, got %v", logs)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
package qmlchecks
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGreeterExternalAuthStatusUsesEffectiveFingerprintAvailability(t *testing.T) {
|
|
||||||
data, err := os.ReadFile("../../../quickshell/Modules/Greetd/GreeterContent.qml")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("read greeter QML: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
content := string(data)
|
|
||||||
for _, required := range []string{
|
|
||||||
"readonly property bool greeterPamHasExternalAuth: greeterPamHasFprint || greeterPamHasU2f",
|
|
||||||
"if (greeterPamHasFprint && greeterPamHasU2f)",
|
|
||||||
"if (greeterPamHasFprint)",
|
|
||||||
} {
|
|
||||||
if !strings.Contains(content, required) {
|
|
||||||
t.Fatalf("greeter external-auth status must contain %q", required)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -260,13 +260,6 @@ func (m Model) installPackages() tea.Cmd {
|
|||||||
// Run optional greeter setup
|
// Run optional greeter setup
|
||||||
if msg.Phase == distros.PhaseComplete && msg.IsComplete && msg.Error == nil {
|
if msg.Phase == distros.PhaseComplete && msg.IsComplete && msg.Error == nil {
|
||||||
if m.optionalDepSelected("dms-greeter") {
|
if m.optionalDepSelected("dms-greeter") {
|
||||||
compositorName := "niri"
|
|
||||||
switch m.selectedWindowManager() {
|
|
||||||
case deps.WindowManagerHyprland:
|
|
||||||
compositorName = "Hyprland"
|
|
||||||
case deps.WindowManagerMango:
|
|
||||||
compositorName = "mango"
|
|
||||||
}
|
|
||||||
m.packageProgressChan <- packageInstallProgressMsg{
|
m.packageProgressChan <- packageInstallProgressMsg{
|
||||||
progress: 0.92,
|
progress: 0.92,
|
||||||
step: "Configuring DMS greeter...",
|
step: "Configuring DMS greeter...",
|
||||||
@@ -279,7 +272,7 @@ func (m Model) installPackages() tea.Cmd {
|
|||||||
logOutput: line,
|
logOutput: line,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := greeter.AutoSetupGreeter(compositorName, m.sudoPassword, greeterLogFunc); err != nil {
|
if err := utils.RunDmsGreeterInstall(m.sudoPassword, greeterLogFunc); err != nil {
|
||||||
m.packageProgressChan <- packageInstallProgressMsg{
|
m.packageProgressChan <- packageInstallProgressMsg{
|
||||||
progress: 0.96,
|
progress: 0.96,
|
||||||
step: "Greeter setup warning",
|
step: "Greeter setup warning",
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunDmsGreeterInstall delegates greeter setup to the standalone dms-greeter
|
||||||
|
// binary, which owns greetd configuration since the greeter moved out of DMS.
|
||||||
|
func RunDmsGreeterInstall(sudoPassword string, logFunc func(string)) error {
|
||||||
|
binary, err := exec.LookPath("dms-greeter")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("dms-greeter binary not found; install the dms-greeter package and run 'dms-greeter install'")
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
if sudoPassword == "" {
|
||||||
|
cmd = exec.Command(binary, "install", "--yes")
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command("sudo", "-S", "-p", "", binary, "install", "--yes")
|
||||||
|
cmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd.Stderr = cmd.Stdout
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
scanner := bufio.NewScanner(stdout)
|
||||||
|
for scanner.Scan() {
|
||||||
|
logFunc(scanner.Text())
|
||||||
|
}
|
||||||
|
return cmd.Wait()
|
||||||
|
}
|
||||||
+1
-1
Submodule dank-qml-common updated: 493b609cc0...c0f2d837e1
@@ -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
|
|
||||||
Suggests: niri | hyprland | sway
|
|
||||||
Description: DankMaterialShell greeter for greetd
|
|
||||||
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
|
||||||
inspired greeter interface built with Quickshell for Wayland compositors.
|
|
||||||
.
|
|
||||||
Supports multiple compositors including Niri, Hyprland, and Sway with automatic
|
|
||||||
compositor detection and configuration. Features session selection, user
|
|
||||||
authentication, and dynamic theming.
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
|
||||||
Upstream-Name: dms-greeter
|
|
||||||
Upstream-Contact: Avenge Media LLC <AvengeMedia.US@gmail.com>
|
|
||||||
Source: https://github.com/AvengeMedia/DankMaterialShell
|
|
||||||
|
|
||||||
Files: *
|
|
||||||
Copyright: 2026 Avenge Media LLC
|
|
||||||
License: MIT
|
|
||||||
|
|
||||||
License: MIT
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
.
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
.
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -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,50 +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 chf - --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 && \
|
|
||||||
install -Dm644 $$SOURCE_DIR/systemd/sysusers-dms-greeter.conf \
|
|
||||||
debian/dms-greeter/usr/lib/sysusers.d/dms-greeter.conf; \
|
|
||||||
else \
|
|
||||||
echo "ERROR: No upstream source (dms-qml or Modules/Greetd/assets/dms-greeter)!" && \
|
|
||||||
echo "Contents of current directory:" && ls -la && exit 1; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Remove build and development files
|
|
||||||
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/core
|
|
||||||
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/distro
|
|
||||||
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/.git*
|
|
||||||
rm -f debian/dms-greeter/usr/share/quickshell/dms-greeter/.gitignore
|
|
||||||
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/.github
|
|
||||||
|
|
||||||
override_dh_auto_clean:
|
|
||||||
rm -rf dms-qml
|
|
||||||
# When build root is dms-qml itself, we're inside it - nothing extra to remove
|
|
||||||
dh_auto_clean
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
3.0 (native)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# OBS _service downloads dms-qml.tar.gz; no extra excludes needed
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
# Spec for DMS Greeter - Stable releases
|
|
||||||
|
|
||||||
%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/systemd/sysusers-dms-greeter.conf %{buildroot}%{_sysusersdir}/dms-greeter.conf
|
|
||||||
|
|
||||||
install -Dm644 %{_builddir}/dms-qml/LICENSE %{buildroot}%{_docdir}/dms-greeter/LICENSE
|
|
||||||
|
|
||||||
install -dm755 %{buildroot}%{_sharedstatedir}/greeter
|
|
||||||
|
|
||||||
# Note: We do NOT install a PAM config here to avoid conflicting w/greetd packages
|
|
||||||
# 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
|
|
||||||
# Remove directories & preserves any user-added files
|
|
||||||
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
|
|
||||||
%license %{_docdir}/dms-greeter/LICENSE
|
|
||||||
%doc %{_docdir}/dms-greeter/README.md
|
|
||||||
%{_bindir}/dms-greeter
|
|
||||||
%{_datadir}/quickshell/dms-greeter/
|
|
||||||
%{_tmpfilesdir}/%{name}.conf
|
|
||||||
%{_sysusersdir}/dms-greeter.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
|
|
||||||
|
|
||||||
# Set SELinux contexts for greeter files on Fedora systems
|
|
||||||
if [ -x /usr/sbin/semanage ] && [ -x /usr/sbin/restorecon ]; then
|
|
||||||
# Greeter launcher binary
|
|
||||||
semanage fcontext -a -t bin_t '%{_bindir}/dms-greeter' >/dev/null 2>&1 || true
|
|
||||||
restorecon %{_bindir}/dms-greeter >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
# Greeter home directory
|
|
||||||
semanage fcontext -a -t user_home_dir_t '%{_sharedstatedir}/greeter(/.*)?' >/dev/null 2>&1 || true
|
|
||||||
restorecon -R %{_sharedstatedir}/greeter >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
# Cache directory for greeter data
|
|
||||||
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
|
|
||||||
|
|
||||||
# Shared data directory
|
|
||||||
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
|
|
||||||
|
|
||||||
# PAM configuration
|
|
||||||
restorecon %{_sysconfdir}/pam.d/greetd >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure proper ownership of greeter directories
|
|
||||||
chown -R greeter:greeter %{_localstatedir}/cache/dms-greeter 2>/dev/null || true
|
|
||||||
chown -R greeter:greeter %{_sharedstatedir}/greeter 2>/dev/null || true
|
|
||||||
|
|
||||||
# Verify PAM configuration - only fix if insufficient
|
|
||||||
PAM_CONFIG="/etc/pam.d/greetd"
|
|
||||||
if [ ! -f "$PAM_CONFIG" ]; 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_selinux.so close
|
|
||||||
session required pam_loginuid.so
|
|
||||||
session required pam_selinux.so open
|
|
||||||
session optional pam_keyinit.so force revoke
|
|
||||||
session include system-auth
|
|
||||||
session include postlogin
|
|
||||||
PAM_EOF
|
|
||||||
chmod 644 "$PAM_CONFIG"
|
|
||||||
# Only show message on initial install
|
|
||||||
[ "$1" -eq 1 ] && echo "Created PAM configuration for greetd"
|
|
||||||
elif ! grep -q "pam_systemd\|system-auth" "$PAM_CONFIG"; then
|
|
||||||
cp "$PAM_CONFIG" "$PAM_CONFIG.backup-dms-greeter"
|
|
||||||
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_selinux.so close
|
|
||||||
session required pam_loginuid.so
|
|
||||||
session required pam_selinux.so open
|
|
||||||
session optional pam_keyinit.so force revoke
|
|
||||||
session include system-auth
|
|
||||||
session include postlogin
|
|
||||||
PAM_EOF
|
|
||||||
chmod 644 "$PAM_CONFIG"
|
|
||||||
# Only show message on initial install
|
|
||||||
[ "$1" -eq 1 ] && echo "Updated PAM configuration (old config backed up to $PAM_CONFIG.backup-dms-greeter)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Auto-configure greetd config
|
|
||||||
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_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
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Only show banner on initial install
|
|
||||||
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
|
|
||||||
|
|
||||||
Next steps:
|
|
||||||
|
|
||||||
1. Enable the greeter:
|
|
||||||
dms greeter enable
|
|
||||||
(This will automatically disable gdm, sddm, or lightdm display managers,
|
|
||||||
set graphical.target, and enable greetd)
|
|
||||||
|
|
||||||
2. Sync your theme with the greeter (optional):
|
|
||||||
If you have DankMaterialShell (DMS) installed, you can sync theming with:
|
|
||||||
dms greeter sync
|
|
||||||
|
|
||||||
3. Check your setup:
|
|
||||||
dms greeter status
|
|
||||||
|
|
||||||
Ready to test? Run: sudo systemctl start greetd or simply log out/reboot your system
|
|
||||||
Documentation: https://danklinux.com/docs/dankgreeter/
|
|
||||||
=========================================================================
|
|
||||||
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
|
|
||||||
%postun
|
|
||||||
# Clean up SELinux contexts on package removal
|
|
||||||
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
|
|
||||||
- Built from GitHub release
|
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
{
|
|
||||||
lib,
|
|
||||||
config,
|
|
||||||
pkgs,
|
|
||||||
dmsPkgs,
|
|
||||||
options,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
inherit (lib) types;
|
|
||||||
cfg = config.programs.dank-material-shell.greeter;
|
|
||||||
cfgDms = config.programs.dank-material-shell;
|
|
||||||
cfgAutoLogin = config.services.displayManager.autoLogin;
|
|
||||||
sessionData = config.services.displayManager.sessionData;
|
|
||||||
|
|
||||||
inherit (config.services.greetd.settings.default_session) user;
|
|
||||||
|
|
||||||
compositorPackage =
|
|
||||||
let
|
|
||||||
configured = lib.attrByPath [ "programs" cfg.compositor.name "package" ] null config;
|
|
||||||
in
|
|
||||||
if configured != null then configured else builtins.getAttr cfg.compositor.name pkgs;
|
|
||||||
|
|
||||||
cacheDir = "/var/lib/dms-greeter";
|
|
||||||
greeterScript = pkgs.writeShellScriptBin "dms-greeter" ''
|
|
||||||
export PATH=$PATH:${
|
|
||||||
lib.makeBinPath [
|
|
||||||
cfg.quickshell.package
|
|
||||||
compositorPackage
|
|
||||||
pkgs.glib # provides gdbus, used by the fprintd hardware probe in GreeterContent.qml
|
|
||||||
pkgs.jq # reads the user's cursor theme from settings.json in dms-greeter
|
|
||||||
]
|
|
||||||
}
|
|
||||||
${
|
|
||||||
lib.escapeShellArgs (
|
|
||||||
[
|
|
||||||
"sh"
|
|
||||||
"${cfg.package}/share/quickshell/dms/Modules/Greetd/assets/dms-greeter"
|
|
||||||
"--cache-dir"
|
|
||||||
cacheDir
|
|
||||||
"--command"
|
|
||||||
cfg.compositor.name
|
|
||||||
"-p"
|
|
||||||
"${cfg.package}/share/quickshell/dms"
|
|
||||||
]
|
|
||||||
++ lib.optionals (cfg.compositor.customConfig != "") [
|
|
||||||
"-C"
|
|
||||||
"${pkgs.writeText "dmsgreeter-compositor-config" cfg.compositor.customConfig}"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
} ${lib.optionalString cfg.logs.save "> ${cfg.logs.path} 2>&1"}
|
|
||||||
'';
|
|
||||||
|
|
||||||
autoLoginCommand =
|
|
||||||
pkgs.runCommand "dms-greeter-autologin-command"
|
|
||||||
{
|
|
||||||
nativeBuildInputs = [
|
|
||||||
pkgs.gnugrep
|
|
||||||
pkgs.coreutils
|
|
||||||
];
|
|
||||||
}
|
|
||||||
''
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
session="${sessionData.autologinSession}"
|
|
||||||
desktops="${sessionData.desktops}"
|
|
||||||
|
|
||||||
for sessionFile in \
|
|
||||||
"$desktops/share/wayland-sessions/$session.desktop" \
|
|
||||||
"$desktops/share/xsessions/$session.desktop"
|
|
||||||
do
|
|
||||||
if [ -f "$sessionFile" ]; then
|
|
||||||
command="$(grep -m1 '^Exec=' "$sessionFile" | cut -d= -f2- || true)"
|
|
||||||
|
|
||||||
if [ -n "$command" ]; then
|
|
||||||
printf '%s\n' "$command" > "$out"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "dms-greeter autologin: could not resolve Exec for session '$session'" >&2
|
|
||||||
exit 1
|
|
||||||
'';
|
|
||||||
|
|
||||||
jq = lib.getExe pkgs.jq;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
imports =
|
|
||||||
let
|
|
||||||
msg = "The option 'programs.dank-material-shell.greeter.compositor.extraConfig' is deprecated. Please use 'programs.dank-material-shell.greeter.compositor.customConfig' instead.";
|
|
||||||
in
|
|
||||||
[
|
|
||||||
(lib.mkRemovedOptionModule [
|
|
||||||
"programs"
|
|
||||||
"dank-material-shell"
|
|
||||||
"greeter"
|
|
||||||
"compositor"
|
|
||||||
"extraConfig"
|
|
||||||
] msg)
|
|
||||||
./dms-rename.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
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"
|
|
||||||
"hyprland"
|
|
||||||
"sway"
|
|
||||||
"labwc"
|
|
||||||
"mango"
|
|
||||||
"scroll"
|
|
||||||
"miracle"
|
|
||||||
];
|
|
||||||
description = "Compositor to run greeter in";
|
|
||||||
};
|
|
||||||
compositor.customConfig = lib.mkOption {
|
|
||||||
type = types.lines;
|
|
||||||
default = "";
|
|
||||||
description = "Custom compositor config";
|
|
||||||
};
|
|
||||||
configFiles = lib.mkOption {
|
|
||||||
type = types.listOf types.path;
|
|
||||||
default = [ ];
|
|
||||||
description = "Config files to copy into data directory";
|
|
||||||
example = [
|
|
||||||
"/home/user/.config/DankMaterialShell/settings.json"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
configHome = lib.mkOption {
|
|
||||||
type = types.nullOr types.str;
|
|
||||||
default = null;
|
|
||||||
example = "/home/user";
|
|
||||||
description = ''
|
|
||||||
User home directory to copy configurations for greeter
|
|
||||||
If DMS config files are in non-standard locations then use the configFiles option instead
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
quickshell = {
|
|
||||||
package = lib.mkOption {
|
|
||||||
default =
|
|
||||||
if (lib.hasAttrByPath [ "programs" "dank-material-shell" "quickshell" "package" ] options) then
|
|
||||||
config.programs.dank-material-shell.quickshell.package
|
|
||||||
else
|
|
||||||
pkgs.quickshell;
|
|
||||||
|
|
||||||
defaultText = ''
|
|
||||||
if (lib.hasAttrByPath [ "programs" "dank-material-shell" "quickshell" "package" ] options) then
|
|
||||||
config.programs.dank-material-shell.quickshell.package
|
|
||||||
else
|
|
||||||
pkgs.quickshell;
|
|
||||||
'';
|
|
||||||
|
|
||||||
description = ''
|
|
||||||
The quickshell package to use (we recommend at least 0.3.0, currently available in nixos-unstable).
|
|
||||||
Defaults to the same set in `programs.dank-material-shell.quickshell.package`, if using the NixOS module.";
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
logs.save = lib.mkEnableOption "saving logs from DMS greeter to file";
|
|
||||||
logs.path = lib.mkOption {
|
|
||||||
type = types.path;
|
|
||||||
default = "/tmp/dms-greeter.log";
|
|
||||||
description = ''
|
|
||||||
File path to save DMS greeter logs to
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
config = lib.mkIf cfg.enable {
|
|
||||||
assertions = [
|
|
||||||
{
|
|
||||||
assertion = (config.users.users.${user} or { }) != { };
|
|
||||||
message = ''
|
|
||||||
dmsgreeter: user set for greetd default_session ${user} does not exist. Please create it before referencing it.
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion = cfgAutoLogin.enable -> sessionData.autologinSession != null;
|
|
||||||
message = ''
|
|
||||||
dms-greeter auto-login requires services.displayManager.defaultSession to be set,
|
|
||||||
or at least one session in services.displayManager.sessionPackages.
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
];
|
|
||||||
# DMS currently relies on /etc/pam.d/login for lock screen password auth on NixOS.
|
|
||||||
# Declare security.pam.services.dankshell only if you want to override that runtime fallback.
|
|
||||||
# Do not add pam_u2f or pam_fprintd here for security-key unlock, enable
|
|
||||||
# programs.dank-material-shell.lockscreen.securityKey.enable, which declares the
|
|
||||||
# dedicated dankshell-u2f service DMS drives on its own.
|
|
||||||
# security.pam.services.dankshell = {
|
|
||||||
# # Example: add faillock
|
|
||||||
# faillock.enable = true;
|
|
||||||
# };
|
|
||||||
services.greetd = {
|
|
||||||
enable = lib.mkDefault true;
|
|
||||||
settings = {
|
|
||||||
default_session.command = lib.mkDefault (lib.getExe greeterScript);
|
|
||||||
initial_session = lib.mkIf (cfgAutoLogin.enable && (cfgAutoLogin.user != null)) {
|
|
||||||
inherit (cfgAutoLogin) user;
|
|
||||||
command = ''${lib.getExe pkgs.bash} -lc "${pkgs.systemd}/bin/systemd-cat $(<${autoLoginCommand})"'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
fonts.packages = with pkgs; [
|
|
||||||
fira-code
|
|
||||||
inter
|
|
||||||
material-symbols
|
|
||||||
];
|
|
||||||
systemd.tmpfiles.settings."10-dmsgreeter" = {
|
|
||||||
${cacheDir}.d = {
|
|
||||||
inherit user;
|
|
||||||
group =
|
|
||||||
if config.users.users.${user}.group != "" then config.users.users.${user}.group else "greeter";
|
|
||||||
mode = "0750";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
systemd.services.greetd.preStart = ''
|
|
||||||
cd ${cacheDir}
|
|
||||||
${lib.concatStringsSep "\n" (
|
|
||||||
lib.map (f: ''
|
|
||||||
if [ -f "${f}" ]; then
|
|
||||||
cp "${f}" .
|
|
||||||
fi
|
|
||||||
'') cfg.configFiles
|
|
||||||
)}
|
|
||||||
|
|
||||||
if [ -f session.json ]; then
|
|
||||||
copy_wallpaper() {
|
|
||||||
local path=$(${jq} -r ".$1 // empty" session.json)
|
|
||||||
if [ -f "$path" ]; then
|
|
||||||
cp "$path" "$2"
|
|
||||||
${jq} ".$1 = \"${cacheDir}/$2\"" session.json > session.tmp
|
|
||||||
mv session.tmp session.json
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
copy_monitor_wallpapers() {
|
|
||||||
${jq} -r ".$1 // {} | to_entries[] | .key + \":\" + .value" session.json 2>/dev/null | while IFS=: read monitor path; do
|
|
||||||
local dest="$2-$(echo "$monitor" | tr -c '[:alnum:]' '-')"
|
|
||||||
if [ -f "$path" ]; then
|
|
||||||
cp "$path" "$dest"
|
|
||||||
${jq} --arg m "$monitor" --arg p "${cacheDir}/$dest" ".$1[\$m] = \$p" session.json > session.tmp
|
|
||||||
mv session.tmp session.json
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
copy_wallpaper "wallpaperPath" "wallpaper"
|
|
||||||
copy_wallpaper "wallpaperPathLight" "wallpaper-light"
|
|
||||||
copy_wallpaper "wallpaperPathDark" "wallpaper-dark"
|
|
||||||
copy_monitor_wallpapers "monitorWallpapers" "wallpaper-monitor"
|
|
||||||
copy_monitor_wallpapers "monitorWallpapersLight" "wallpaper-monitor-light"
|
|
||||||
copy_monitor_wallpapers "monitorWallpapersDark" "wallpaper-monitor-dark"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f settings.json ]; then
|
|
||||||
theme_file="$(${jq} -r '.customThemeFile // empty' settings.json)"
|
|
||||||
if [ -f "$theme_file" ] && [ -r "$theme_file" ]; then
|
|
||||||
cp "$theme_file" custom-theme.json
|
|
||||||
mv settings.json settings.orig.json
|
|
||||||
${jq} '.customThemeFile = "${cacheDir}/custom-theme.json"' settings.orig.json > settings.json
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
mv dms-colors.json colors.json || :
|
|
||||||
chown ${user}: * || :
|
|
||||||
'';
|
|
||||||
programs.dank-material-shell.greeter.configFiles = lib.mkIf (cfg.configHome != null) [
|
|
||||||
"${cfg.configHome}/.config/DankMaterialShell/settings.json"
|
|
||||||
"${cfg.configHome}/.local/state/DankMaterialShell/session.json"
|
|
||||||
"${cfg.configHome}/.cache/DankMaterialShell/dms-colors.json"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,6 @@ rec {
|
|||||||
paths = [
|
paths = [
|
||||||
nixos-module
|
nixos-module
|
||||||
nixos-service-start-module
|
nixos-service-start-module
|
||||||
greeter-niri-module
|
|
||||||
niri-home-module
|
niri-home-module
|
||||||
home-manager-module
|
home-manager-module
|
||||||
];
|
];
|
||||||
@@ -29,13 +28,6 @@ rec {
|
|||||||
;
|
;
|
||||||
};
|
};
|
||||||
|
|
||||||
greeter-niri-module = import ./greeter-niri-module.nix {
|
|
||||||
inherit
|
|
||||||
self
|
|
||||||
pkgs
|
|
||||||
;
|
|
||||||
};
|
|
||||||
|
|
||||||
niri-home-module = import ./niri-home-module.nix {
|
niri-home-module = import ./niri-home-module.nix {
|
||||||
inherit
|
inherit
|
||||||
self
|
self
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
{
|
|
||||||
self,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
pkgs.testers.runNixOSTest {
|
|
||||||
name = "dms-greeter-niri-module";
|
|
||||||
|
|
||||||
nodes.machine = {
|
|
||||||
imports = [
|
|
||||||
self.nixosModules.greeter
|
|
||||||
];
|
|
||||||
|
|
||||||
users.groups.greeter = { };
|
|
||||||
users.users.greeter = {
|
|
||||||
isSystemUser = true;
|
|
||||||
group = "greeter";
|
|
||||||
};
|
|
||||||
users.users.alice.isNormalUser = true;
|
|
||||||
|
|
||||||
services.greetd.settings.default_session.user = "greeter";
|
|
||||||
services.displayManager.autoLogin = {
|
|
||||||
enable = true;
|
|
||||||
user = "alice";
|
|
||||||
};
|
|
||||||
services.displayManager.defaultSession = "niri";
|
|
||||||
|
|
||||||
programs.niri.enable = true;
|
|
||||||
|
|
||||||
programs.dank-material-shell.greeter = {
|
|
||||||
enable = true;
|
|
||||||
compositor.name = "niri";
|
|
||||||
};
|
|
||||||
|
|
||||||
system.stateVersion = "25.11";
|
|
||||||
};
|
|
||||||
|
|
||||||
testScript = ''
|
|
||||||
import re
|
|
||||||
|
|
||||||
machine.wait_for_unit("multi-user.target")
|
|
||||||
machine.wait_for_unit("greetd.service")
|
|
||||||
|
|
||||||
machine.succeed("systemctl is-enabled greetd.service")
|
|
||||||
machine.succeed("systemctl is-active greetd.service")
|
|
||||||
|
|
||||||
greetd_unit = machine.succeed("cat /etc/systemd/system/greetd.service")
|
|
||||||
config_match = re.search(r'--config (/nix/store[^ ]+-greetd.toml)', greetd_unit)
|
|
||||||
if config_match is None:
|
|
||||||
raise AssertionError(greetd_unit)
|
|
||||||
|
|
||||||
greetd_config_path = config_match.group(1)
|
|
||||||
greetd_config = machine.succeed(f"cat {greetd_config_path}")
|
|
||||||
t.assertIn("dms-greeter", greetd_config)
|
|
||||||
t.assertIn("[initial_session]", greetd_config)
|
|
||||||
|
|
||||||
initial_session = greetd_config.split("[initial_session]", 1)[1]
|
|
||||||
t.assertIn('user = "alice"', initial_session)
|
|
||||||
t.assertIn("systemd-cat", initial_session)
|
|
||||||
|
|
||||||
script_match = re.search(r'command\s*=\s*"([^"]+/bin/dms-greeter)"', greetd_config)
|
|
||||||
if script_match is None:
|
|
||||||
raise AssertionError(greetd_config)
|
|
||||||
|
|
||||||
script_path = script_match.group(1)
|
|
||||||
script = machine.succeed(f"cat {script_path}")
|
|
||||||
t.assertIn("--command", script)
|
|
||||||
t.assertIn("niri", script)
|
|
||||||
t.assertIn("/share/quickshell/dms", script)
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
@@ -1,325 +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/systemd/sysusers-dms-greeter.conf %{buildroot}%{_sysusersdir}/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
|
|
||||||
%{_sysusersdir}/dms-greeter.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
|
|
||||||
@@ -5,7 +5,6 @@ set -euo pipefail
|
|||||||
# Usage: ./copr-upload.sh [PACKAGE] [VERSION] [RELEASE]
|
# Usage: ./copr-upload.sh [PACKAGE] [VERSION] [RELEASE]
|
||||||
# Examples:
|
# Examples:
|
||||||
# ./copr-upload.sh dms 1.0.3 1
|
# ./copr-upload.sh dms 1.0.3 1
|
||||||
# ./copr-upload.sh dms-greeter 1.0.3 1
|
|
||||||
|
|
||||||
PACKAGE="${1:-dms}"
|
PACKAGE="${1:-dms}"
|
||||||
VERSION="${2:-}"
|
VERSION="${2:-}"
|
||||||
@@ -17,11 +16,9 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|||||||
# Determine Copr project based on package
|
# Determine Copr project based on package
|
||||||
if [ "$PACKAGE" = "dms" ]; then
|
if [ "$PACKAGE" = "dms" ]; then
|
||||||
COPR_PROJECT="avengemedia/dms"
|
COPR_PROJECT="avengemedia/dms"
|
||||||
elif [ "$PACKAGE" = "dms-greeter" ]; then
|
|
||||||
COPR_PROJECT="avengemedia/danklinux"
|
|
||||||
else
|
else
|
||||||
echo "❌ Unknown package: $PACKAGE"
|
echo "❌ Unknown package: $PACKAGE"
|
||||||
echo "Supported packages: dms, dms-greeter"
|
echo "Supported packages: dms"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
OBS_BASE_PROJECT="home:AvengeMedia"
|
OBS_BASE_PROJECT="home:AvengeMedia"
|
||||||
OBS_BASE="$HOME/.cache/osc-checkouts"
|
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")
|
REPOS=("Debian_13" "openSUSE_Tumbleweed" "16.0")
|
||||||
ARCHES=("x86_64" "aarch64")
|
ARCHES=("x86_64" "aarch64")
|
||||||
@@ -41,9 +41,6 @@ for pkg in "${PACKAGES[@]}"; do
|
|||||||
dms-git)
|
dms-git)
|
||||||
PROJECT="$OBS_BASE_PROJECT:dms-git"
|
PROJECT="$OBS_BASE_PROJECT:dms-git"
|
||||||
;;
|
;;
|
||||||
dms-greeter)
|
|
||||||
PROJECT="$OBS_BASE_PROJECT:danklinux"
|
|
||||||
;;
|
|
||||||
*)
|
*)
|
||||||
echo "Error: Unknown package '$pkg'"
|
echo "Error: Unknown package '$pkg'"
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -68,14 +68,13 @@ fi
|
|||||||
|
|
||||||
OBS_BASE_PROJECT="home:AvengeMedia"
|
OBS_BASE_PROJECT="home:AvengeMedia"
|
||||||
OBS_BASE="$HOME/.cache/osc-checkouts"
|
OBS_BASE="$HOME/.cache/osc-checkouts"
|
||||||
AVAILABLE_PACKAGES=(dms dms-git dms-greeter)
|
AVAILABLE_PACKAGES=(dms dms-git)
|
||||||
|
|
||||||
if [[ -z "$PACKAGE" ]]; then
|
if [[ -z "$PACKAGE" ]]; then
|
||||||
echo "Available packages:"
|
echo "Available packages:"
|
||||||
echo ""
|
echo ""
|
||||||
echo " 1. dms - Stable DMS"
|
echo " 1. dms - Stable DMS"
|
||||||
echo " 2. dms-git - Nightly DMS"
|
echo " 2. dms-git - Nightly DMS"
|
||||||
echo " 3. dms-greeter - DMS greeter for greetd"
|
|
||||||
echo " a. all"
|
echo " a. all"
|
||||||
echo ""
|
echo ""
|
||||||
read -r -p "Select package (1-${#AVAILABLE_PACKAGES[@]}, a): " selection
|
read -r -p "Select package (1-${#AVAILABLE_PACKAGES[@]}, a): " selection
|
||||||
@@ -221,22 +220,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"
|
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() {
|
update_opensuse_git_spec() {
|
||||||
local spec_path="$1"
|
local spec_path="$1"
|
||||||
local go_ver
|
local go_ver
|
||||||
@@ -323,9 +306,6 @@ dms)
|
|||||||
dms-git)
|
dms-git)
|
||||||
PROJECT="dms-git"
|
PROJECT="dms-git"
|
||||||
;;
|
;;
|
||||||
dms-greeter)
|
|
||||||
PROJECT="danklinux"
|
|
||||||
;;
|
|
||||||
*)
|
*)
|
||||||
echo "Error: Unknown package '$PACKAGE'"
|
echo "Error: Unknown package '$PACKAGE'"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -425,8 +405,6 @@ if [[ -d "distro/debian/$PACKAGE/debian" ]]; then
|
|||||||
# Keep Debian _service in sync with changelog version
|
# Keep Debian _service in sync with changelog version
|
||||||
if [[ "$PACKAGE" == "dms" ]] && [[ -f "distro/debian/$PACKAGE/_service" ]]; then
|
if [[ "$PACKAGE" == "dms" ]] && [[ -f "distro/debian/$PACKAGE/_service" ]]; then
|
||||||
update_debian_dms_service "distro/debian/$PACKAGE/_service"
|
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
|
fi
|
||||||
|
|
||||||
# Check if this version already exists in OBS
|
# Check if this version already exists in OBS
|
||||||
@@ -480,16 +458,6 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ -f "distro/opensuse/$PACKAGE.spec" ]];
|
|||||||
|
|
||||||
if [[ "$PACKAGE" == *"-git" ]]; then
|
if [[ "$PACKAGE" == *"-git" ]]; then
|
||||||
update_opensuse_git_spec "$WORK_DIR/$PACKAGE.spec"
|
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
|
||||||
|
|
||||||
if [[ -f "$WORK_DIR/.osc/$PACKAGE.spec" ]]; then
|
if [[ -f "$WORK_DIR/.osc/$PACKAGE.spec" ]]; then
|
||||||
@@ -555,24 +523,6 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ "$UPLOAD_DEBIAN" == false ]] && [[ -f
|
|||||||
fi
|
fi
|
||||||
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
|
if [[ -n "$SOURCE_DIR" && -d "$SOURCE_DIR" ]]; then
|
||||||
SOURCE0=$(grep "^Source0:" "distro/opensuse/$PACKAGE.spec" | awk '{print $2}' | head -1)
|
SOURCE0=$(grep "^Source0:" "distro/opensuse/$PACKAGE.spec" | awk '{print $2}' | head -1)
|
||||||
|
|
||||||
@@ -581,15 +531,6 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ "$UPLOAD_DEBIAN" == false ]] && [[ -f
|
|||||||
cd "$OBS_TARBALL_DIR"
|
cd "$OBS_TARBALL_DIR"
|
||||||
|
|
||||||
case "$PACKAGE" in
|
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 -czhf "$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)
|
||||||
DMS_VERSION=$(grep "^Version:" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec" | sed 's/^Version:[[:space:]]*//' | head -1)
|
DMS_VERSION=$(grep "^Version:" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec" | sed 's/^Version:[[:space:]]*//' | head -1)
|
||||||
EXPECTED_DIR="DankMaterialShell-${DMS_VERSION}"
|
EXPECTED_DIR="DankMaterialShell-${DMS_VERSION}"
|
||||||
@@ -727,17 +668,6 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
|
|||||||
if [[ -z "$SOURCE_DIR" ]]; then
|
if [[ -z "$SOURCE_DIR" ]]; then
|
||||||
SOURCE_DIR=$(find . -maxdepth 1 -type d ! -name "." | head -1)
|
SOURCE_DIR=$(find . -maxdepth 1 -type d ! -name "." | head -1)
|
||||||
fi
|
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
|
if [[ -z "$SOURCE_DIR" || ! -d "$SOURCE_DIR" ]]; then
|
||||||
echo "Error: Failed to extract source archive or find source directory"
|
echo "Error: Failed to extract source archive or find source directory"
|
||||||
echo "Contents of $TEMP_DIR:"
|
echo "Contents of $TEMP_DIR:"
|
||||||
@@ -814,21 +744,6 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
|
|||||||
cd "$OBS_TARBALL_DIR"
|
cd "$OBS_TARBALL_DIR"
|
||||||
|
|
||||||
case "$PACKAGE" in
|
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 -cJhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
|
||||||
elif [[ "$SOURCE0" == *.tar.bz2 ]]; then
|
|
||||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
|
||||||
else
|
|
||||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czhf "$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)
|
||||||
DMS_VERSION=$(grep "^Version:" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec" | sed 's/^Version:[[:space:]]*//' | head -1)
|
DMS_VERSION=$(grep "^Version:" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec" | sed 's/^Version:[[:space:]]*//' | head -1)
|
||||||
EXPECTED_DIR="DankMaterialShell-${DMS_VERSION}"
|
EXPECTED_DIR="DankMaterialShell-${DMS_VERSION}"
|
||||||
@@ -888,16 +803,6 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
|
|||||||
cp "distro/opensuse/$PACKAGE.spec" "$WORK_DIR/"
|
cp "distro/opensuse/$PACKAGE.spec" "$WORK_DIR/"
|
||||||
if [[ "$PACKAGE" == *"-git" ]]; then
|
if [[ "$PACKAGE" == *"-git" ]]; then
|
||||||
update_opensuse_git_spec "$WORK_DIR/$PACKAGE.spec"
|
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
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -1029,47 +934,10 @@ EOF
|
|||||||
echo " - Quilt format detected: creating debian.tar.gz"
|
echo " - Quilt format detected: creating debian.tar.gz"
|
||||||
tar -czf "$WORK_DIR/debian.tar.gz" -C "distro/debian/$PACKAGE" debian/
|
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_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)
|
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"
|
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" <<DSCEOF
|
||||||
Format: 3.0 (quilt)
|
Format: 3.0 (quilt)
|
||||||
Source: $PACKAGE
|
Source: $PACKAGE
|
||||||
@@ -1078,7 +946,7 @@ Architecture: any
|
|||||||
Version: $VERSION
|
Version: $VERSION
|
||||||
Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
|
Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
|
||||||
Build-Depends: debhelper-compat (= 13), wget, gzip
|
Build-Depends: debhelper-compat (= 13), wget, gzip
|
||||||
${DEBTRANSFORM_EXTRA}Files:${DSC_FILES_DEBIAN}
|
Files:
|
||||||
$DEBIAN_MD5 $DEBIAN_SIZE debian.tar.gz
|
$DEBIAN_MD5 $DEBIAN_SIZE debian.tar.gz
|
||||||
DSCEOF
|
DSCEOF
|
||||||
fi
|
fi
|
||||||
@@ -1117,13 +985,6 @@ if [[ -n "$OBS_FILES" ]]; then
|
|||||||
continue
|
continue
|
||||||
fi
|
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
|
if [[ "$old_file" == "${PACKAGE}-source.tar.gz" ]]; then
|
||||||
echo " - Keeping source tarball: $old_file"
|
echo " - Keeping source tarball: $old_file"
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -118,7 +118,6 @@ get_ppa_name() {
|
|||||||
case "$pkg" in
|
case "$pkg" in
|
||||||
dms) echo "dms" ;;
|
dms) echo "dms" ;;
|
||||||
dms-git) echo "dms-git" ;;
|
dms-git) echo "dms-git" ;;
|
||||||
dms-greeter) echo "danklinux" ;;
|
|
||||||
*) echo "" ;;
|
*) echo "" ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@@ -227,9 +226,6 @@ dms-git)
|
|||||||
dms)
|
dms)
|
||||||
GIT_REPO="AvengeMedia/DankMaterialShell"
|
GIT_REPO="AvengeMedia/DankMaterialShell"
|
||||||
;;
|
;;
|
||||||
dms-greeter)
|
|
||||||
GIT_REPO="AvengeMedia/DankMaterialShell"
|
|
||||||
;;
|
|
||||||
danksearch)
|
danksearch)
|
||||||
GIT_REPO="AvengeMedia/danksearch"
|
GIT_REPO="AvengeMedia/danksearch"
|
||||||
;;
|
;;
|
||||||
@@ -352,18 +348,6 @@ EOF
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
dms-greeter)
|
|
||||||
info "Downloading source for dms-greeter..."
|
|
||||||
if [ ! -f "dms-greeter-source.tar.gz" ]; then
|
|
||||||
info "Downloading dms-greeter source..."
|
|
||||||
if wget -O dms-greeter-source.tar.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-source.tar.gz"; then
|
|
||||||
success "source tarball downloaded"
|
|
||||||
else
|
|
||||||
error "Failed to download dms-greeter-source.tar.gz"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ LAUNCHPAD_API="https://api.launchpad.net/1.0"
|
|||||||
DISTRO_SERIES_LIST=(resolute stonking)
|
DISTRO_SERIES_LIST=(resolute stonking)
|
||||||
|
|
||||||
# Define packages (sync with ppa-upload.sh)
|
# Define packages (sync with ppa-upload.sh)
|
||||||
ALL_PACKAGES=(dms dms-git dms-greeter)
|
ALL_PACKAGES=(dms dms-git)
|
||||||
|
|
||||||
# Function to get PPA name for a package
|
# Function to get PPA name for a package
|
||||||
get_ppa_name() {
|
get_ppa_name() {
|
||||||
@@ -22,7 +22,6 @@ get_ppa_name() {
|
|||||||
case "$pkg" in
|
case "$pkg" in
|
||||||
dms) echo "dms" ;;
|
dms) echo "dms" ;;
|
||||||
dms-git) echo "dms-git" ;;
|
dms-git) echo "dms-git" ;;
|
||||||
dms-greeter) echo "danklinux" ;;
|
|
||||||
*) echo "" ;;
|
*) echo "" ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@@ -72,7 +71,7 @@ elif [[ -n "$PPA_INPUT" ]]; then
|
|||||||
else
|
else
|
||||||
# Check all packages in all PPAs
|
# Check all packages in all PPAs
|
||||||
PACKAGES=("${ALL_PACKAGES[@]}")
|
PACKAGES=("${ALL_PACKAGES[@]}")
|
||||||
PPAS=("dms" "dms-git" "danklinux")
|
PPAS=("dms" "dms-git")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Function to get build status color and symbol
|
# Function to get build status color and symbol
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ JSON=false
|
|||||||
PACKAGES=(
|
PACKAGES=(
|
||||||
"dms:dms:release"
|
"dms:dms:release"
|
||||||
"dms-git:dms-git:git"
|
"dms-git:dms-git:git"
|
||||||
"dms-greeter:danklinux:release"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
|||||||
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||||
error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||||
|
|
||||||
AVAILABLE_PACKAGES=(dms dms-git dms-greeter)
|
AVAILABLE_PACKAGES=(dms dms-git)
|
||||||
|
|
||||||
KEEP_BUILDS=false
|
KEEP_BUILDS=false
|
||||||
REBUILD_RELEASE=""
|
REBUILD_RELEASE=""
|
||||||
@@ -101,7 +101,6 @@ get_ppa_name() {
|
|||||||
case "$pkg" in
|
case "$pkg" in
|
||||||
dms) echo "dms" ;;
|
dms) echo "dms" ;;
|
||||||
dms-git) echo "dms-git" ;;
|
dms-git) echo "dms-git" ;;
|
||||||
dms-greeter) echo "danklinux" ;;
|
|
||||||
*) echo "" ;;
|
*) echo "" ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@@ -336,7 +335,7 @@ echo
|
|||||||
|
|
||||||
info "Step 2: Uploading to PPA..."
|
info "Step 2: Uploading to PPA..."
|
||||||
|
|
||||||
if [ "$PPA_NAME" = "danklinux" ] || [ "$PPA_NAME" = "dms" ] || [ "$PPA_NAME" = "dms-git" ]; then
|
if [ "$PPA_NAME" = "dms" ] || [ "$PPA_NAME" = "dms-git" ]; then
|
||||||
warn "Using lftp for upload"
|
warn "Using lftp for upload"
|
||||||
|
|
||||||
BUILD_DIR=$(dirname "$CHANGES_FILE")
|
BUILD_DIR=$(dirname "$CHANGES_FILE")
|
||||||
@@ -410,9 +409,9 @@ EOF
|
|||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# This branch should not be reached for DMS packages
|
# This branch should not be reached for DMS packages
|
||||||
# All DMS packages (dms, dms-git, dms-greeter) use lftp
|
# All DMS packages (dms, dms-git) use lftp
|
||||||
error "Unknown PPA: $PPA_NAME"
|
error "Unknown PPA: $PPA_NAME"
|
||||||
error "DMS packages use lftp for upload. Supported PPAs: dms, dms-git, danklinux"
|
error "DMS packages use lftp for upload. Supported PPAs: dms, dms-git"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -484,13 +483,6 @@ if [ "$KEEP_BUILDS" = "false" ]; then
|
|||||||
REMOVED=$((REMOVED + 1))
|
REMOVED=$((REMOVED + 1))
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
dms-greeter)
|
|
||||||
# Remove downloaded source
|
|
||||||
if [ -f "$PACKAGE_DIR/dms-greeter-source.tar.gz" ]; then
|
|
||||||
rm -f "$PACKAGE_DIR/dms-greeter-source.tar.gz"
|
|
||||||
REMOVED=$((REMOVED + 1))
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [ $REMOVED -gt 0 ]; then
|
if [ $REMOVED -gt 0 ]; then
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
dms-greeter (1.0.2ppa2) questing; urgency=medium
|
|
||||||
|
|
||||||
* Rebuild for packaging fixes (ppa2)
|
|
||||||
|
|
||||||
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 06:50:44 +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
|
|
||||||
Suggests: niri | hyprland | sway
|
|
||||||
Description: DankMaterialShell greeter for greetd
|
|
||||||
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
|
||||||
inspired greeter interface built with Quickshell for Wayland compositors.
|
|
||||||
.
|
|
||||||
Supports multiple compositors including Niri, Hyprland, and Sway with automatic
|
|
||||||
compositor detection and configuration. Features session selection, user
|
|
||||||
authentication, and dynamic theming.
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
|
||||||
Upstream-Name: dms-greeter
|
|
||||||
Upstream-Contact: Avenge Media LLC <AvengeMedia.US@gmail.com>
|
|
||||||
Source: https://github.com/AvengeMedia/DankMaterialShell
|
|
||||||
|
|
||||||
Files: *
|
|
||||||
Copyright: 2025 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,67 +0,0 @@
|
|||||||
#!/usr/bin/make -f
|
|
||||||
|
|
||||||
export DH_VERBOSE = 1
|
|
||||||
|
|
||||||
# Extract version from debian/changelog
|
|
||||||
DEB_VERSION := $(shell dpkg-parsechangelog -S Version)
|
|
||||||
# Get upstream version (strip -1ppa1 suffix)
|
|
||||||
UPSTREAM_VERSION := $(shell echo $(DEB_VERSION) | sed 's/-[^-]*$$//')
|
|
||||||
BASE_VERSION := $(shell echo $(UPSTREAM_VERSION) | sed 's/ppa[0-9]*$$//' | sed 's/+git.*//')
|
|
||||||
|
|
||||||
%:
|
|
||||||
dh $@
|
|
||||||
|
|
||||||
override_dh_auto_build:
|
|
||||||
# All files are included in source package
|
|
||||||
test -f dms-greeter-source.tar.gz || (echo "ERROR: dms-greeter-source.tar.gz not found!" && exit 1)
|
|
||||||
|
|
||||||
# Extract source tarball
|
|
||||||
tar -xzf dms-greeter-source.tar.gz
|
|
||||||
# Find the extracted directory
|
|
||||||
SOURCE_DIR=$$(find . -maxdepth 1 -type d -name "DankMaterialShell*" | head -n1); \
|
|
||||||
if [ -n "$$SOURCE_DIR" ]; then \
|
|
||||||
ln -sf $$SOURCE_DIR DankMaterialShell-$(BASE_VERSION); \
|
|
||||||
fi
|
|
||||||
|
|
||||||
override_dh_auto_install:
|
|
||||||
# Install greeter files to shared data location
|
|
||||||
mkdir -p debian/dms-greeter/usr/share/quickshell/dms-greeter
|
|
||||||
cp -rL DankMaterialShell-$(BASE_VERSION)/quickshell/* debian/dms-greeter/usr/share/quickshell/dms-greeter/
|
|
||||||
|
|
||||||
# Install launcher script
|
|
||||||
install -Dm755 DankMaterialShell-$(BASE_VERSION)/quickshell/Modules/Greetd/assets/dms-greeter \
|
|
||||||
debian/dms-greeter/usr/bin/dms-greeter
|
|
||||||
|
|
||||||
# Install documentation
|
|
||||||
install -Dm644 DankMaterialShell-$(BASE_VERSION)/quickshell/Modules/Greetd/README.md \
|
|
||||||
debian/dms-greeter/usr/share/doc/dms-greeter/README.md
|
|
||||||
|
|
||||||
# Install LICENSE file
|
|
||||||
install -Dm644 DankMaterialShell-$(BASE_VERSION)/LICENSE \
|
|
||||||
debian/dms-greeter/usr/share/doc/dms-greeter/LICENSE
|
|
||||||
|
|
||||||
# Install systemd tmpfiles/sysusers fragments only when present in the fetched source.
|
|
||||||
# sysusers-dms-greeter.conf landed upstream after v1.4.6; guarding both lets older
|
|
||||||
# release tarballs build, while future tags that ship the files install them automatically.
|
|
||||||
if [ -f DankMaterialShell-$(BASE_VERSION)/quickshell/systemd/tmpfiles-dms-greeter.conf ]; then \
|
|
||||||
install -Dpm0644 DankMaterialShell-$(BASE_VERSION)/quickshell/systemd/tmpfiles-dms-greeter.conf \
|
|
||||||
debian/dms-greeter/usr/lib/tmpfiles.d/dms-greeter.conf; \
|
|
||||||
fi
|
|
||||||
if [ -f DankMaterialShell-$(BASE_VERSION)/quickshell/systemd/sysusers-dms-greeter.conf ]; then \
|
|
||||||
install -Dm644 DankMaterialShell-$(BASE_VERSION)/quickshell/systemd/sysusers-dms-greeter.conf \
|
|
||||||
debian/dms-greeter/usr/lib/sysusers.d/dms-greeter.conf; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create cache directory structure (will be created by postinst)
|
|
||||||
mkdir -p debian/dms-greeter/var/cache/dms-greeter
|
|
||||||
|
|
||||||
# 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 DankMaterialShell-*
|
|
||||||
dh_auto_clean
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
3.0 (native)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
dms-greeter-source.tar.gz
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# Include files that are normally excluded by .gitignore
|
|
||||||
# These are needed for the build process on Launchpad (which has no internet access)
|
|
||||||
tar-ignore = !dms-greeter-source.tar.gz
|
|
||||||
@@ -5,7 +5,6 @@ XBPS templates for DankMaterialShell on [Void Linux](https://voidlinux.org).
|
|||||||
| Package | Source repo | Template |
|
| Package | Source repo | Template |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `dms` | DankMaterialShell | [`srcpkgs/dms/template`](srcpkgs/dms/template) |
|
| `dms` | DankMaterialShell | [`srcpkgs/dms/template`](srcpkgs/dms/template) |
|
||||||
| `dms-greeter` (optional) | DankMaterialShell | [`srcpkgs/dms-greeter/template`](srcpkgs/dms-greeter/template) |
|
|
||||||
| `dgop` | AvengeMedia/dgop | maintained in the **danklinux** repo (`distro/void/srcpkgs/dgop`) |
|
| `dgop` | AvengeMedia/dgop | maintained in the **danklinux** repo (`distro/void/srcpkgs/dgop`) |
|
||||||
| `danksearch` | AvengeMedia/danksearch | maintained in the **danklinux** repo (`distro/void/srcpkgs/danksearch`) |
|
| `danksearch` | AvengeMedia/danksearch | maintained in the **danklinux** repo (`distro/void/srcpkgs/danksearch`) |
|
||||||
|
|
||||||
@@ -83,7 +82,6 @@ Inside a `void-packages` checkout (symlink or copy these `srcpkgs/<pkg>` dirs in
|
|||||||
./xbps-src pkg dgop
|
./xbps-src pkg dgop
|
||||||
./xbps-src pkg danksearch
|
./xbps-src pkg danksearch
|
||||||
./xbps-src pkg dms
|
./xbps-src pkg dms
|
||||||
./xbps-src pkg dms-greeter # optional
|
|
||||||
|
|
||||||
# lint (xlint ships in the xtools package)
|
# lint (xlint ships in the xtools package)
|
||||||
xlint srcpkgs/dms/template
|
xlint srcpkgs/dms/template
|
||||||
@@ -125,26 +123,3 @@ sudo ln -sf /etc/sv/elogind /var/service/elogind
|
|||||||
```
|
```
|
||||||
|
|
||||||
The `dankinstall` Void path enables both services after installing packages.
|
The `dankinstall` Void path enables both services after installing packages.
|
||||||
|
|
||||||
## Greeter (optional)
|
|
||||||
|
|
||||||
Install `dms-greeter`, then let the CLI do the setup:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
dms greeter enable # configures greetd + the Void seat/PAM bits below
|
|
||||||
dms greeter sync # optional: share theming with the shell
|
|
||||||
```
|
|
||||||
|
|
||||||
`dms-greeter` requires D-Bus and elogind. `dms greeter enable` enables the
|
|
||||||
`dbus` and `elogind` runit services, configures greetd for elogind
|
|
||||||
(`LIBSEAT_BACKEND=logind`), adds `_greeter` to the `video` and `input` groups,
|
|
||||||
and adds `pam_rundir` to `/etc/pam.d/greetd` (so the post-login session gets an
|
|
||||||
`XDG_RUNTIME_DIR`). It disables seatd if enabled: on Void, seatd is the
|
|
||||||
alternative to elogind, and running both fights over the seat. Greeter sessions
|
|
||||||
are launched through `dbus-run-session`. A Wayland compositor and a working DRM
|
|
||||||
device (`/dev/dri/card*`) are required and not pulled in automatically.
|
|
||||||
|
|
||||||
`dms greeter enable` also rewrites `/etc/sv/greetd/run` to wait for the `dbus`
|
|
||||||
and `elogind` services, preventing a first-boot race that can leave the greeter
|
|
||||||
on a black screen. greetd package updates restore the stock run script; re-run
|
|
||||||
`dms greeter enable` afterwards.
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
dms-greeter installed.
|
|
||||||
|
|
||||||
Configure and enable it with:
|
|
||||||
|
|
||||||
dms greeter enable
|
|
||||||
|
|
||||||
This points greetd at the greeter and sets up everything Void needs that logind
|
|
||||||
would handle on systemd: enables seatd, adds the greeter user to the seat/video/
|
|
||||||
input groups, and adds pam_rundir to the greetd PAM stack. Optionally sync your
|
|
||||||
shell theme into the greeter with:
|
|
||||||
|
|
||||||
dms greeter sync
|
|
||||||
|
|
||||||
Requirements not pulled in automatically: a Wayland compositor (niri, hyprland,
|
|
||||||
sway, …) and a working DRM device (/dev/dri/card*; in a VM, enable virtio-gpu).
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# Template file for 'dms-greeter'
|
|
||||||
#
|
|
||||||
# greetd greeter for DankMaterialShell
|
|
||||||
# Builds from the same DMS release tarball as 'dms'; keep version/checksum in sync.
|
|
||||||
# CI rewrites version/checksum from the selected release tag before building.
|
|
||||||
# Setup is done by `dms greeter enable`, not by this package — see distro/void/README.md.
|
|
||||||
pkgname=dms-greeter
|
|
||||||
version=1.5.0
|
|
||||||
revision=2
|
|
||||||
short_desc="DankMaterialShell greeter for greetd"
|
|
||||||
maintainer="AvengeMedia <AvengeMedia.US@gmail.com>"
|
|
||||||
license="MIT"
|
|
||||||
homepage="https://danklinux.com"
|
|
||||||
distfiles="https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${version}/dms-source.tar.gz"
|
|
||||||
checksum=14f2678d6a15223ba069d1b8c8a21a5564b735d190c231f62ed44fd8bf48677c
|
|
||||||
|
|
||||||
depends="greetd quickshell acl-progs dbus elogind pam_rundir mesa-dri"
|
|
||||||
|
|
||||||
# Cache dir the greeter uses as $HOME (owned by greetd's _greeter user).
|
|
||||||
make_dirs="/var/cache/dms-greeter 0750 _greeter _greeter"
|
|
||||||
|
|
||||||
do_install() {
|
|
||||||
# Launcher wrapper -> /usr/bin/dms-greeter
|
|
||||||
vbin quickshell/Modules/Greetd/assets/dms-greeter
|
|
||||||
|
|
||||||
# Same QML tree as the shell; greeter mode is selected at runtime via DMS_RUN_GREETER.
|
|
||||||
vmkdir usr/share/quickshell/dms-greeter
|
|
||||||
vcopy "quickshell/*" usr/share/quickshell/dms-greeter
|
|
||||||
|
|
||||||
# Sample compositor configs for reference
|
|
||||||
vinstall quickshell/Modules/Greetd/assets/dms-niri.kdl 644 usr/share/dms-greeter
|
|
||||||
vinstall quickshell/Modules/Greetd/assets/dms-hypr.conf 644 usr/share/dms-greeter
|
|
||||||
|
|
||||||
vdoc quickshell/Modules/Greetd/README.md
|
|
||||||
vlicense LICENSE
|
|
||||||
}
|
|
||||||
Generated
+2
-2
@@ -3,11 +3,11 @@
|
|||||||
"dank-qml-common": {
|
"dank-qml-common": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1784471445,
|
"lastModified": 1784485083,
|
||||||
"narHash": "sha256-hRTmcd3Ms9DF3X35ZiapXM15rG5X1DtIdlrQZwRINFE=",
|
"narHash": "sha256-hRTmcd3Ms9DF3X35ZiapXM15rG5X1DtIdlrQZwRINFE=",
|
||||||
"owner": "AvengeMedia",
|
"owner": "AvengeMedia",
|
||||||
"repo": "dank-qml-common",
|
"repo": "dank-qml-common",
|
||||||
"rev": "493b609cc04659f1388f5738e8f65d6c531994fe",
|
"rev": "c0f2d837e1c9933e6789eeb38e7d456088bd5d1b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -159,9 +159,6 @@
|
|||||||
--replace-fail /usr/bin/dms $out/bin/dms \
|
--replace-fail /usr/bin/dms $out/bin/dms \
|
||||||
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
|
--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/assets/pam/fprint \
|
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
|
||||||
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so \
|
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so \
|
||||||
--replace-fail pam_deny.so ${pkgs.pam}/lib/security/pam_deny.so \
|
--replace-fail pam_deny.so ${pkgs.pam}/lib/security/pam_deny.so \
|
||||||
@@ -222,7 +219,7 @@
|
|||||||
|
|
||||||
nixosModules.default = self.nixosModules.dank-material-shell;
|
nixosModules.default = self.nixosModules.dank-material-shell;
|
||||||
|
|
||||||
nixosModules.greeter = mkModuleWithDmsPkgs ./distro/nix/greeter.nix;
|
nixosModules.greeter = builtins.warn "dank-material-shell: the greeter moved to the dank-greeter repo; use `inputs.dank-greeter.nixosModules.default` and `programs.dms-greeter` (https://github.com/AvengeMedia/dank-greeter)" { };
|
||||||
|
|
||||||
nixosModules.dankMaterialShell = builtins.warn "dank-material-shell: flake output `nixosModules.dankMaterialShell` has been renamed to `nixosModules.dank-material-shell`" self.nixosModules.dank-material-shell;
|
nixosModules.dankMaterialShell = builtins.warn "dank-material-shell: flake output `nixosModules.dankMaterialShell` has been renamed to `nixosModules.dank-material-shell`" self.nixosModules.dank-material-shell;
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ Singleton {
|
|||||||
|
|
||||||
readonly property int cacheConfigVersion: 1
|
readonly property int cacheConfigVersion: 1
|
||||||
|
|
||||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
|
||||||
|
|
||||||
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericCacheLocation)
|
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericCacheLocation)
|
||||||
readonly property string _stateDir: Paths.strip(_stateUrl)
|
readonly property string _stateDir: Paths.strip(_stateUrl)
|
||||||
|
|
||||||
@@ -75,10 +73,8 @@ Singleton {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (!isGreeterMode) {
|
|
||||||
loadCache();
|
loadCache();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function loadCache() {
|
function loadCache() {
|
||||||
_loading = true;
|
_loading = true;
|
||||||
@@ -200,7 +196,7 @@ Singleton {
|
|||||||
FileView {
|
FileView {
|
||||||
id: launcherCacheFile
|
id: launcherCacheFile
|
||||||
|
|
||||||
path: isGreeterMode ? "" : _stateDir + "/DankMaterialShell/launcher_cache.json"
|
path: _stateDir + "/DankMaterialShell/launcher_cache.json"
|
||||||
blockLoading: true
|
blockLoading: true
|
||||||
blockWrites: true
|
blockWrites: true
|
||||||
atomicWrites: true
|
atomicWrites: true
|
||||||
@@ -210,20 +206,16 @@ Singleton {
|
|||||||
FileView {
|
FileView {
|
||||||
id: cacheFile
|
id: cacheFile
|
||||||
|
|
||||||
path: isGreeterMode ? "" : _stateDir + "/DankMaterialShell/cache.json"
|
path: _stateDir + "/DankMaterialShell/cache.json"
|
||||||
blockLoading: true
|
blockLoading: true
|
||||||
blockWrites: true
|
blockWrites: true
|
||||||
atomicWrites: true
|
atomicWrites: true
|
||||||
watchChanges: !isGreeterMode
|
watchChanges: true
|
||||||
onLoaded: {
|
onLoaded: {
|
||||||
if (!isGreeterMode) {
|
|
||||||
parseCache(cacheFile.text());
|
parseCache(cacheFile.text());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
onLoadFailed: error => {
|
onLoadFailed: error => {
|
||||||
if (!isGreeterMode) {
|
|
||||||
log.info("No cache file found, starting fresh");
|
log.info("No cache file found, starting fresh");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
.pragma library
|
|
||||||
|
|
||||||
// English language name -> ISO 639-1 code, for abbreviating xkb layout names.
|
|
||||||
const LANG_CODES = {
|
|
||||||
"afrikaans": "af",
|
|
||||||
"albanian": "sq",
|
|
||||||
"amharic": "am",
|
|
||||||
"arabic": "ar",
|
|
||||||
"armenian": "hy",
|
|
||||||
"azerbaijani": "az",
|
|
||||||
"basque": "eu",
|
|
||||||
"belarusian": "be",
|
|
||||||
"bengali": "bn",
|
|
||||||
"bosnian": "bs",
|
|
||||||
"bulgarian": "bg",
|
|
||||||
"burmese": "my",
|
|
||||||
"catalan": "ca",
|
|
||||||
"chinese": "zh",
|
|
||||||
"croatian": "hr",
|
|
||||||
"czech": "cs",
|
|
||||||
"danish": "da",
|
|
||||||
"dutch": "nl",
|
|
||||||
"english": "en",
|
|
||||||
"esperanto": "eo",
|
|
||||||
"estonian": "et",
|
|
||||||
"filipino": "fil",
|
|
||||||
"finnish": "fi",
|
|
||||||
"french": "fr",
|
|
||||||
"galician": "gl",
|
|
||||||
"georgian": "ka",
|
|
||||||
"german": "de",
|
|
||||||
"greek": "el",
|
|
||||||
"gujarati": "gu",
|
|
||||||
"hausa": "ha",
|
|
||||||
"hebrew": "he",
|
|
||||||
"hindi": "hi",
|
|
||||||
"hungarian": "hu",
|
|
||||||
"icelandic": "is",
|
|
||||||
"igbo": "ig",
|
|
||||||
"indonesian": "id",
|
|
||||||
"irish": "ga",
|
|
||||||
"italian": "it",
|
|
||||||
"japanese": "ja",
|
|
||||||
"javanese": "jv",
|
|
||||||
"kannada": "kn",
|
|
||||||
"kazakh": "kk",
|
|
||||||
"khmer": "km",
|
|
||||||
"korean": "ko",
|
|
||||||
"kurdish": "ku",
|
|
||||||
"kyrgyz": "ky",
|
|
||||||
"lao": "lo",
|
|
||||||
"latvian": "lv",
|
|
||||||
"lithuanian": "lt",
|
|
||||||
"luxembourgish": "lb",
|
|
||||||
"macedonian": "mk",
|
|
||||||
"malay": "ms",
|
|
||||||
"malayalam": "ml",
|
|
||||||
"maltese": "mt",
|
|
||||||
"maori": "mi",
|
|
||||||
"marathi": "mr",
|
|
||||||
"mongolian": "mn",
|
|
||||||
"nepali": "ne",
|
|
||||||
"norwegian": "no",
|
|
||||||
"pashto": "ps",
|
|
||||||
"persian": "fa",
|
|
||||||
"iranian": "fa",
|
|
||||||
"farsi": "fa",
|
|
||||||
"polish": "pl",
|
|
||||||
"portuguese": "pt",
|
|
||||||
"punjabi": "pa",
|
|
||||||
"romanian": "ro",
|
|
||||||
"russian": "ru",
|
|
||||||
"serbian": "sr",
|
|
||||||
"sindhi": "sd",
|
|
||||||
"sinhala": "si",
|
|
||||||
"slovak": "sk",
|
|
||||||
"slovenian": "sl",
|
|
||||||
"somali": "so",
|
|
||||||
"spanish": "es",
|
|
||||||
"swahili": "sw",
|
|
||||||
"swedish": "sv",
|
|
||||||
"tajik": "tg",
|
|
||||||
"tamil": "ta",
|
|
||||||
"tatar": "tt",
|
|
||||||
"telugu": "te",
|
|
||||||
"thai": "th",
|
|
||||||
"tibetan": "bo",
|
|
||||||
"turkish": "tr",
|
|
||||||
"turkmen": "tk",
|
|
||||||
"ukrainian": "uk",
|
|
||||||
"urdu": "ur",
|
|
||||||
"uyghur": "ug",
|
|
||||||
"uzbek": "uz",
|
|
||||||
"vietnamese": "vi",
|
|
||||||
"welsh": "cy",
|
|
||||||
"yiddish": "yi",
|
|
||||||
"yoruba": "yo",
|
|
||||||
"zulu": "zu"
|
|
||||||
};
|
|
||||||
|
|
||||||
function layoutCode(layoutName) {
|
|
||||||
if (!layoutName)
|
|
||||||
return "";
|
|
||||||
const lang = layoutName.split(" ")[0].toLowerCase();
|
|
||||||
const code = LANG_CODES[lang] || lang.substring(0, 2);
|
|
||||||
return code.toUpperCase();
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,6 @@ Singleton {
|
|||||||
|
|
||||||
readonly property int sessionConfigVersion: 3
|
readonly property int sessionConfigVersion: 3
|
||||||
|
|
||||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
|
||||||
property bool _parseError: false
|
property bool _parseError: false
|
||||||
property bool _hasLoaded: false
|
property bool _hasLoaded: false
|
||||||
property bool _isReadOnly: false
|
property bool _isReadOnly: false
|
||||||
@@ -227,10 +226,8 @@ Singleton {
|
|||||||
property string settingsSidebarCollapsedIds: ","
|
property string settingsSidebarCollapsedIds: ","
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (!isGreeterMode) {
|
|
||||||
loadSettings();
|
loadSettings();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
property var _pendingMigration: null
|
property var _pendingMigration: null
|
||||||
|
|
||||||
@@ -238,11 +235,6 @@ Singleton {
|
|||||||
_hasUnsavedChanges = false;
|
_hasUnsavedChanges = false;
|
||||||
_pendingMigration = null;
|
_pendingMigration = null;
|
||||||
|
|
||||||
if (isGreeterMode) {
|
|
||||||
parseSettings(greeterSessionFile.text());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const txt = settingsFile.text();
|
const txt = settingsFile.text();
|
||||||
let obj = (txt && txt.trim()) ? JSON.parse(txt) : null;
|
let obj = (txt && txt.trim()) ? JSON.parse(txt) : null;
|
||||||
@@ -280,7 +272,7 @@ Singleton {
|
|||||||
_loadedSessionSnapshot = getCurrentSessionJson();
|
_loadedSessionSnapshot = getCurrentSessionJson();
|
||||||
_hasLoaded = true;
|
_hasLoaded = true;
|
||||||
|
|
||||||
if (!isGreeterMode && typeof Theme !== "undefined")
|
if (typeof Theme !== "undefined")
|
||||||
Theme.generateSystemThemesFromCurrentTheme();
|
Theme.generateSystemThemesFromCurrentTheme();
|
||||||
|
|
||||||
if (typeof WallpaperCyclingService !== "undefined")
|
if (typeof WallpaperCyclingService !== "undefined")
|
||||||
@@ -362,7 +354,7 @@ Singleton {
|
|||||||
_loadedSessionSnapshot = getCurrentSessionJson();
|
_loadedSessionSnapshot = getCurrentSessionJson();
|
||||||
_hasLoaded = true;
|
_hasLoaded = true;
|
||||||
|
|
||||||
if (!isGreeterMode && typeof Theme !== "undefined")
|
if (typeof Theme !== "undefined")
|
||||||
Theme.generateSystemThemesFromCurrentTheme();
|
Theme.generateSystemThemesFromCurrentTheme();
|
||||||
|
|
||||||
if (typeof WallpaperCyclingService !== "undefined")
|
if (typeof WallpaperCyclingService !== "undefined")
|
||||||
@@ -386,7 +378,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveSettings() {
|
function saveSettings() {
|
||||||
if (isGreeterMode || _parseError || !_hasLoaded)
|
if (_parseError || !_hasLoaded)
|
||||||
return;
|
return;
|
||||||
settingsFile.setText(getCurrentSessionJson());
|
settingsFile.setText(getCurrentSessionJson());
|
||||||
if (_isReadOnly)
|
if (_isReadOnly)
|
||||||
@@ -1394,56 +1386,21 @@ Singleton {
|
|||||||
FileView {
|
FileView {
|
||||||
id: settingsFile
|
id: settingsFile
|
||||||
|
|
||||||
path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json"
|
path: StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json"
|
||||||
blockLoading: true
|
blockLoading: true
|
||||||
blockWrites: true
|
blockWrites: true
|
||||||
atomicWrites: true
|
atomicWrites: true
|
||||||
watchChanges: !isGreeterMode
|
watchChanges: true
|
||||||
onLoaded: {
|
onLoaded: {
|
||||||
if (!isGreeterMode) {
|
|
||||||
_hasUnsavedChanges = false;
|
_hasUnsavedChanges = false;
|
||||||
parseSettings(settingsFile.text());
|
parseSettings(settingsFile.text());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
onSaveFailed: error => {
|
onSaveFailed: error => {
|
||||||
root._isReadOnly = true;
|
root._isReadOnly = true;
|
||||||
root._hasUnsavedChanges = root._checkForUnsavedChanges();
|
root._hasUnsavedChanges = root._checkForUnsavedChanges();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property string _greeterCacheDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
|
|
||||||
|
|
||||||
property string greeterSessionBaseDir: root._greeterCacheDir
|
|
||||||
|
|
||||||
function setGreeterSessionBaseDir(dir) {
|
|
||||||
const next = dir || root._greeterCacheDir;
|
|
||||||
if (greeterSessionBaseDir === next)
|
|
||||||
return;
|
|
||||||
greeterSessionBaseDir = next;
|
|
||||||
if (isGreeterMode)
|
|
||||||
greeterSessionFile.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetGreeterSessionBaseDir() {
|
|
||||||
setGreeterSessionBaseDir(root._greeterCacheDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: greeterSessionFile
|
|
||||||
|
|
||||||
path: root.greeterSessionBaseDir ? (root.greeterSessionBaseDir + "/session.json") : ""
|
|
||||||
preload: isGreeterMode
|
|
||||||
blockLoading: false
|
|
||||||
blockWrites: true
|
|
||||||
watchChanges: false
|
|
||||||
printErrors: true
|
|
||||||
onLoaded: {
|
|
||||||
if (isGreeterMode) {
|
|
||||||
parseSettings(greeterSessionFile.text());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: sessionWritableCheckProcess
|
id: sessionWritableCheckProcess
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ Singleton {
|
|||||||
|
|
||||||
readonly property int settingsConfigVersion: 12
|
readonly property int settingsConfigVersion: 12
|
||||||
|
|
||||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
|
||||||
|
|
||||||
enum Position {
|
enum Position {
|
||||||
Top,
|
Top,
|
||||||
Bottom,
|
Bottom,
|
||||||
@@ -1439,20 +1437,16 @@ Singleton {
|
|||||||
signal workspaceIconsUpdated
|
signal workspaceIconsUpdated
|
||||||
|
|
||||||
function refreshAuthAvailability() {
|
function refreshAuthAvailability() {
|
||||||
if (isGreeterMode)
|
|
||||||
return;
|
|
||||||
Processes.detectAuthCapabilities();
|
Processes.detectAuthCapabilities();
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (!isGreeterMode) {
|
|
||||||
Processes.settingsRoot = root;
|
Processes.settingsRoot = root;
|
||||||
loadSettings();
|
loadSettings();
|
||||||
initializeListModels();
|
initializeListModels();
|
||||||
refreshAuthAvailability();
|
refreshAuthAvailability();
|
||||||
Processes.checkPluginSettings();
|
Processes.checkPluginSettings();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function applyStoredTheme() {
|
function applyStoredTheme() {
|
||||||
if (typeof Theme !== "undefined") {
|
if (typeof Theme !== "undefined") {
|
||||||
@@ -1517,8 +1511,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkIconThemeDrift() {
|
function checkIconThemeDrift() {
|
||||||
if (isGreeterMode)
|
|
||||||
return;
|
|
||||||
if (resolveIconTheme() === "System Default")
|
if (resolveIconTheme() === "System Default")
|
||||||
return;
|
return;
|
||||||
if (!lastAppliedIconTheme)
|
if (!lastAppliedIconTheme)
|
||||||
@@ -1674,8 +1666,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scheduleAuthApply() {
|
function scheduleAuthApply() {
|
||||||
if (isGreeterMode)
|
|
||||||
return;
|
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
Processes.settingsRoot = root;
|
Processes.settingsRoot = root;
|
||||||
Processes.scheduleAuthApply();
|
Processes.scheduleAuthApply();
|
||||||
@@ -1683,8 +1673,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scheduleGreeterAutoLoginSync() {
|
function scheduleGreeterAutoLoginSync() {
|
||||||
if (isGreeterMode)
|
|
||||||
return;
|
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
Processes.settingsRoot = root;
|
Processes.settingsRoot = root;
|
||||||
Processes.scheduleGreeterAutoLoginSync();
|
Processes.scheduleGreeterAutoLoginSync();
|
||||||
@@ -1692,8 +1680,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function markGreeterSyncPending(who, key, oldValue) {
|
function markGreeterSyncPending(who, key, oldValue) {
|
||||||
if (isGreeterMode)
|
|
||||||
return;
|
|
||||||
if (!(key in greeterSyncBaseline)) {
|
if (!(key in greeterSyncBaseline)) {
|
||||||
var baseline = greeterSyncBaseline;
|
var baseline = greeterSyncBaseline;
|
||||||
baseline[key] = oldValue;
|
baseline[key] = oldValue;
|
||||||
@@ -3632,7 +3618,7 @@ Singleton {
|
|||||||
FileView {
|
FileView {
|
||||||
id: settingsFile
|
id: settingsFile
|
||||||
|
|
||||||
path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json"
|
path: StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json"
|
||||||
blockLoading: true
|
blockLoading: true
|
||||||
blockWrites: true
|
blockWrites: true
|
||||||
atomicWrites: true
|
atomicWrites: true
|
||||||
@@ -3645,8 +3631,6 @@ Singleton {
|
|||||||
settingsFileReloadDebounce.restart();
|
settingsFileReloadDebounce.restart();
|
||||||
}
|
}
|
||||||
onLoaded: {
|
onLoaded: {
|
||||||
if (isGreeterMode)
|
|
||||||
return;
|
|
||||||
const wasLoaded = _hasLoaded;
|
const wasLoaded = _hasLoaded;
|
||||||
const prevFrameEnabled = frameEnabled;
|
const prevFrameEnabled = frameEnabled;
|
||||||
const prevFrameMode = frameMode;
|
const prevFrameMode = frameMode;
|
||||||
@@ -3689,10 +3673,8 @@ Singleton {
|
|||||||
updateFrameCompositorLayout();
|
updateFrameCompositorLayout();
|
||||||
}
|
}
|
||||||
onLoadFailed: error => {
|
onLoadFailed: error => {
|
||||||
if (!isGreeterMode) {
|
|
||||||
applyStoredTheme();
|
applyStoredTheme();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
onSaveFailed: error => {
|
onSaveFailed: error => {
|
||||||
root._isReadOnly = true;
|
root._isReadOnly = true;
|
||||||
root._hasUnsavedChanges = root._checkForUnsavedChanges();
|
root._hasUnsavedChanges = root._checkForUnsavedChanges();
|
||||||
@@ -3702,26 +3684,22 @@ Singleton {
|
|||||||
FileView {
|
FileView {
|
||||||
id: pluginSettingsFile
|
id: pluginSettingsFile
|
||||||
|
|
||||||
path: isGreeterMode ? "" : pluginSettingsPath
|
path: pluginSettingsPath
|
||||||
blockLoading: true
|
blockLoading: true
|
||||||
blockWrites: true
|
blockWrites: true
|
||||||
atomicWrites: true
|
atomicWrites: true
|
||||||
printErrors: false
|
printErrors: false
|
||||||
watchChanges: !isGreeterMode
|
watchChanges: true
|
||||||
onLoaded: {
|
onLoaded: {
|
||||||
if (!isGreeterMode) {
|
|
||||||
parsePluginSettings(pluginSettingsFile.text());
|
parsePluginSettings(pluginSettingsFile.text());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
onLoadFailed: error => {
|
onLoadFailed: error => {
|
||||||
if (!isGreeterMode) {
|
|
||||||
const msg = String(error || "");
|
const msg = String(error || "");
|
||||||
if (!_isMissingPluginSettingsError(error))
|
if (!_isMissingPluginSettingsError(error))
|
||||||
log.warn("Failed to load plugin_settings.json. Error:", msg);
|
log.warn("Failed to load plugin_settings.json. Error:", msg);
|
||||||
_resetPluginSettings();
|
_resetPluginSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
property bool pluginSettingsFileExists: false
|
property bool pluginSettingsFileExists: false
|
||||||
|
|
||||||
|
|||||||
+16
-86
@@ -8,7 +8,6 @@ import Quickshell.Io
|
|||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.DankCommon.Common as DankCommon
|
import qs.DankCommon.Common as DankCommon
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Modules.Greetd
|
|
||||||
import "StockThemes.js" as StockThemes
|
import "StockThemes.js" as StockThemes
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
@@ -143,13 +142,11 @@ Singleton {
|
|||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
Quickshell.execDetached(["mkdir", "-p", stateDir]);
|
Quickshell.execDetached(["mkdir", "-p", stateDir]);
|
||||||
if (typeof SessionData === "undefined" || !SessionData.isGreeterMode)
|
|
||||||
Quickshell.execDetached([shellDir + "/scripts/gtk.sh", configDir, "", shellDir, "assets-only"]);
|
Quickshell.execDetached([shellDir + "/scripts/gtk.sh", configDir, "", shellDir, "assets-only"]);
|
||||||
Proc.runCommand("matugenCheck", ["sh", "-c", "command -v matugen"], (output, code) => {
|
Proc.runCommand("matugenCheck", ["sh", "-c", "command -v matugen"], (output, code) => {
|
||||||
matugenAvailable = (code === 0) && !envDisableMatugen;
|
matugenAvailable = (code === 0) && !envDisableMatugen;
|
||||||
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode);
|
|
||||||
|
|
||||||
if (!matugenAvailable || isGreeterMode) {
|
if (!matugenAvailable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,13 +425,6 @@ Singleton {
|
|||||||
DMSService.sendRequest("theme.auto.trigger", {});
|
DMSService.sendRequest("theme.auto.trigger", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyGreeterTheme(themeName) {
|
|
||||||
switchTheme(themeName, false, false);
|
|
||||||
if (themeName === dynamic && dynamicColorsFileView.path) {
|
|
||||||
dynamicColorsFileView.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMatugenColor(path, fallback) {
|
function getMatugenColor(path, fallback) {
|
||||||
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark";
|
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark";
|
||||||
let cur = matugenColors && matugenColors.colors && matugenColors.colors[colorMode];
|
let cur = matugenColors && matugenColors.colors && matugenColors.colors[colorMode];
|
||||||
@@ -1222,26 +1212,11 @@ Singleton {
|
|||||||
return presetMap[SettingsData.modalAnimationSpeed] ?? 150;
|
return presetMap[SettingsData.modalAnimationSpeed] ?? 150;
|
||||||
}
|
}
|
||||||
|
|
||||||
property real cornerRadius: {
|
property real cornerRadius: typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12
|
||||||
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
|
|
||||||
return GreetdSettings.cornerRadius;
|
|
||||||
}
|
|
||||||
return typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12;
|
|
||||||
}
|
|
||||||
|
|
||||||
property string fontFamily: {
|
property string fontFamily: typeof SettingsData !== "undefined" ? resolvedFontFamily(SettingsData.fontFamily) : DankCommon.Fonts.sans
|
||||||
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
|
|
||||||
return resolvedFontFamily(GreetdSettings.getEffectiveFontFamily());
|
|
||||||
}
|
|
||||||
return typeof SettingsData !== "undefined" ? resolvedFontFamily(SettingsData.fontFamily) : DankCommon.Fonts.sans;
|
|
||||||
}
|
|
||||||
|
|
||||||
property string monoFontFamily: {
|
property string monoFontFamily: typeof SettingsData !== "undefined" ? resolvedMonoFontFamily(SettingsData.monoFontFamily) : DankCommon.Fonts.mono
|
||||||
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
|
|
||||||
return resolvedMonoFontFamily(GreetdSettings.monoFontFamily);
|
|
||||||
}
|
|
||||||
return typeof SettingsData !== "undefined" ? resolvedMonoFontFamily(SettingsData.monoFontFamily) : DankCommon.Fonts.mono;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolvedFontFamily(family) {
|
function resolvedFontFamily(family) {
|
||||||
if (family === defaultFontFamily)
|
if (family === defaultFontFamily)
|
||||||
@@ -1255,19 +1230,9 @@ Singleton {
|
|||||||
return family;
|
return family;
|
||||||
}
|
}
|
||||||
|
|
||||||
property int fontWeight: {
|
property int fontWeight: typeof SettingsData !== "undefined" ? SettingsData.fontWeight : Font.Normal
|
||||||
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
|
|
||||||
return GreetdSettings.fontWeight;
|
|
||||||
}
|
|
||||||
return typeof SettingsData !== "undefined" ? SettingsData.fontWeight : Font.Normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
property real fontScale: {
|
property real fontScale: typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0
|
||||||
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
|
|
||||||
return GreetdSettings.fontScale;
|
|
||||||
}
|
|
||||||
return typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
property real spacingXXS: 2
|
property real spacingXXS: 2
|
||||||
property real spacingXS: 4
|
property real spacingXS: 4
|
||||||
@@ -1327,16 +1292,13 @@ Singleton {
|
|||||||
currentThemeCategory = "generic";
|
currentThemeCategory = "generic";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode);
|
if (savePrefs && typeof SettingsData !== "undefined") {
|
||||||
if (savePrefs && typeof SettingsData !== "undefined" && !isGreeterMode) {
|
|
||||||
SettingsData.set("currentThemeCategory", currentThemeCategory);
|
SettingsData.set("currentThemeCategory", currentThemeCategory);
|
||||||
SettingsData.set("currentThemeName", currentTheme);
|
SettingsData.set("currentThemeName", currentTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isGreeterMode) {
|
|
||||||
generateSystemThemesFromCurrentTheme();
|
generateSystemThemesFromCurrentTheme();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function setLightMode(light, savePrefs = true, enableTransition = false) {
|
function setLightMode(light, savePrefs = true, enableTransition = false) {
|
||||||
if (enableTransition) {
|
if (enableTransition) {
|
||||||
@@ -1347,19 +1309,16 @@ Singleton {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode);
|
if (savePrefs && typeof SessionData !== "undefined") {
|
||||||
if (savePrefs && typeof SessionData !== "undefined" && !isGreeterMode) {
|
|
||||||
SessionData.setLightMode(light);
|
SessionData.setLightMode(light);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isGreeterMode) {
|
|
||||||
PortalService.setLightMode(light);
|
PortalService.setLightMode(light);
|
||||||
if (typeof SettingsData !== "undefined") {
|
if (typeof SettingsData !== "undefined") {
|
||||||
SettingsData.updateCosmicThemeMode(light);
|
SettingsData.updateCosmicThemeMode(light);
|
||||||
}
|
}
|
||||||
generateSystemThemesFromCurrentTheme();
|
generateSystemThemesFromCurrentTheme();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function toggleLightMode(savePrefs = true) {
|
function toggleLightMode(savePrefs = true) {
|
||||||
setLightMode(!isLightMode, savePrefs, true);
|
setLightMode(!isLightMode, savePrefs, true);
|
||||||
@@ -1412,8 +1371,7 @@ Singleton {
|
|||||||
if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) {
|
if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) {
|
||||||
const defaults = themeData.variants.defaults || {};
|
const defaults = themeData.variants.defaults || {};
|
||||||
const modeDefaults = defaults[colorMode] || defaults.dark || {};
|
const modeDefaults = defaults[colorMode] || defaults.dark || {};
|
||||||
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
const stored = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults, colorMode) : modeDefaults;
|
||||||
const stored = isGreeterMode ? (GreetdSettings.registryThemeVariants[themeId]?.[colorMode] || modeDefaults) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults, colorMode) : modeDefaults);
|
|
||||||
var flavorId = stored.flavor || modeDefaults.flavor || "";
|
var flavorId = stored.flavor || modeDefaults.flavor || "";
|
||||||
const accentId = stored.accent || modeDefaults.accent || "";
|
const accentId = stored.accent || modeDefaults.accent || "";
|
||||||
var flavor = findVariant(themeData.variants.flavors, flavorId);
|
var flavor = findVariant(themeData.variants.flavors, flavorId);
|
||||||
@@ -1439,8 +1397,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (themeData.variants.options && themeData.variants.options.length > 0) {
|
if (themeData.variants.options && themeData.variants.options.length > 0) {
|
||||||
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
const selectedVariantId = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, themeData.variants.default) : themeData.variants.default;
|
||||||
const selectedVariantId = isGreeterMode ? (typeof GreetdSettings.registryThemeVariants[themeId] === "string" ? GreetdSettings.registryThemeVariants[themeId] : themeData.variants.default) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, themeData.variants.default) : themeData.variants.default);
|
|
||||||
const variant = findVariant(themeData.variants.options, selectedVariantId);
|
const variant = findVariant(themeData.variants.options, selectedVariantId);
|
||||||
if (variant) {
|
if (variant) {
|
||||||
const variantColors = variant[colorMode] || variant.dark || variant.light || {};
|
const variantColors = variant[colorMode] || variant.dark || variant.light || {};
|
||||||
@@ -1800,8 +1757,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function generateSystemThemesFromCurrentTheme() {
|
function generateSystemThemesFromCurrentTheme() {
|
||||||
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode);
|
if (!matugenAvailable)
|
||||||
if (!matugenAvailable || isGreeterMode)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_lastGenerateMs = Date.now();
|
_lastGenerateMs = Date.now();
|
||||||
@@ -1842,9 +1798,8 @@ Singleton {
|
|||||||
const defaults = customThemeRawData.variants.defaults || {};
|
const defaults = customThemeRawData.variants.defaults || {};
|
||||||
const darkDefaults = defaults.dark || {};
|
const darkDefaults = defaults.dark || {};
|
||||||
const lightDefaults = defaults.light || defaults.dark || {};
|
const lightDefaults = defaults.light || defaults.dark || {};
|
||||||
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
const storedDark = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults;
|
||||||
const storedDark = isGreeterMode ? (GreetdSettings.registryThemeVariants[themeId]?.dark || darkDefaults) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults);
|
const storedLight = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults;
|
||||||
const storedLight = isGreeterMode ? (GreetdSettings.registryThemeVariants[themeId]?.light || lightDefaults) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults);
|
|
||||||
const darkFlavorId = storedDark.flavor || darkDefaults.flavor || "";
|
const darkFlavorId = storedDark.flavor || darkDefaults.flavor || "";
|
||||||
const lightFlavorId = storedLight.flavor || lightDefaults.flavor || "";
|
const lightFlavorId = storedLight.flavor || lightDefaults.flavor || "";
|
||||||
const darkAccentId = storedDark.accent || darkDefaults.accent || "";
|
const darkAccentId = storedDark.accent || darkDefaults.accent || "";
|
||||||
@@ -1864,8 +1819,7 @@ Singleton {
|
|||||||
lightTheme = mergeColors(lightTheme, lightAccent[lightFlavor.id] || {});
|
lightTheme = mergeColors(lightTheme, lightAccent[lightFlavor.id] || {});
|
||||||
}
|
}
|
||||||
} else if (customThemeRawData.variants.options) {
|
} else if (customThemeRawData.variants.options) {
|
||||||
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
const selectedVariantId = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, customThemeRawData.variants.default) : customThemeRawData.variants.default;
|
||||||
const selectedVariantId = isGreeterMode ? (typeof GreetdSettings.registryThemeVariants[themeId] === "string" ? GreetdSettings.registryThemeVariants[themeId] : customThemeRawData.variants.default) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, customThemeRawData.variants.default) : customThemeRawData.variants.default);
|
|
||||||
const variant = findVariant(customThemeRawData.variants.options, selectedVariantId);
|
const variant = findVariant(customThemeRawData.variants.options, selectedVariantId);
|
||||||
if (variant) {
|
if (variant) {
|
||||||
darkTheme = mergeColors(darkTheme, variant.dark || {});
|
darkTheme = mergeColors(darkTheme, variant.dark || {});
|
||||||
@@ -2214,32 +2168,11 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property string _greeterCacheDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
|
|
||||||
|
|
||||||
property string greeterColorsBaseDir: root._greeterCacheDir
|
|
||||||
|
|
||||||
function setGreeterColorsBaseDir(dir) {
|
|
||||||
const next = dir || root._greeterCacheDir;
|
|
||||||
if (greeterColorsBaseDir === next)
|
|
||||||
return;
|
|
||||||
greeterColorsBaseDir = next;
|
|
||||||
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode)
|
|
||||||
dynamicColorsFileView.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetGreeterColorsBaseDir() {
|
|
||||||
setGreeterColorsBaseDir(root._greeterCacheDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: dynamicColorsFileView
|
id: dynamicColorsFileView
|
||||||
path: {
|
path: stateDir + "/dms-colors.json"
|
||||||
if (SessionData.isGreeterMode)
|
|
||||||
return root.greeterColorsBaseDir ? (root.greeterColorsBaseDir + "/colors.json") : "";
|
|
||||||
return stateDir + "/dms-colors.json";
|
|
||||||
}
|
|
||||||
blockLoading: false
|
blockLoading: false
|
||||||
watchChanges: !SessionData.isGreeterMode
|
watchChanges: true
|
||||||
|
|
||||||
function parseAndLoadColors() {
|
function parseAndLoadColors() {
|
||||||
try {
|
try {
|
||||||
@@ -2274,9 +2207,6 @@ Singleton {
|
|||||||
if (currentTheme !== dynamic)
|
if (currentTheme !== dynamic)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (SessionData.isGreeterMode)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (workerRunning) {
|
if (workerRunning) {
|
||||||
colorsReloadRetry.restart();
|
colorsReloadRetry.restart();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ Singleton {
|
|||||||
property var settingsRoot: null
|
property var settingsRoot: null
|
||||||
|
|
||||||
onSettingsRootChanged: {
|
onSettingsRootChanged: {
|
||||||
if (settingsRoot && !settingsRoot.isGreeterMode)
|
if (settingsRoot)
|
||||||
consumeGreeterAutoLoginPendingSync();
|
consumeGreeterAutoLoginPendingSync();
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property string greeterAutoLoginPendingSyncPath: (Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter") + "/.local/state/auto-login-sync-pending"
|
readonly property string greeterAutoLoginPendingSyncPath: (Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter") + "/.local/state/auto-login-sync-pending"
|
||||||
|
|
||||||
function consumeGreeterAutoLoginPendingSync() {
|
function consumeGreeterAutoLoginPendingSync() {
|
||||||
if (!settingsRoot || settingsRoot.isGreeterMode)
|
if (!settingsRoot)
|
||||||
return;
|
return;
|
||||||
greeterAutoLoginPendingCheckProcess.running = true;
|
greeterAutoLoginPendingCheckProcess.running = true;
|
||||||
}
|
}
|
||||||
@@ -287,7 +287,7 @@ Singleton {
|
|||||||
property string authApplyTerminalFallbackStderr: ""
|
property string authApplyTerminalFallbackStderr: ""
|
||||||
|
|
||||||
function scheduleAuthApply() {
|
function scheduleAuthApply() {
|
||||||
if (!settingsRoot || settingsRoot.isGreeterMode)
|
if (!settingsRoot)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
authApplyQueued = true;
|
authApplyQueued = true;
|
||||||
@@ -300,7 +300,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function beginAuthApply() {
|
function beginAuthApply() {
|
||||||
if (!authApplyQueued || authApplyRunning || !settingsRoot || settingsRoot.isGreeterMode)
|
if (!authApplyQueued || authApplyRunning || !settingsRoot)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
authApplyQueued = false;
|
authApplyQueued = false;
|
||||||
@@ -339,7 +339,7 @@ Singleton {
|
|||||||
property string greeterAutoLoginSyncStderr: ""
|
property string greeterAutoLoginSyncStderr: ""
|
||||||
|
|
||||||
function scheduleGreeterAutoLoginSync() {
|
function scheduleGreeterAutoLoginSync() {
|
||||||
if (!settingsRoot || settingsRoot.isGreeterMode)
|
if (!settingsRoot)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
greeterAutoLoginSyncQueued = true;
|
greeterAutoLoginSyncQueued = true;
|
||||||
@@ -352,7 +352,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function beginGreeterAutoLoginSync() {
|
function beginGreeterAutoLoginSync() {
|
||||||
if (!greeterAutoLoginSyncQueued || greeterAutoLoginSyncRunning || !settingsRoot || settingsRoot.isGreeterMode)
|
if (!greeterAutoLoginSyncQueued || greeterAutoLoginSyncRunning || !settingsRoot)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
greeterAutoLoginSyncQueued = false;
|
greeterAutoLoginSyncQueued = false;
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import qs.Modules.Greetd
|
|
||||||
|
|
||||||
Scope {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
GreeterSurface {}
|
|
||||||
}
|
|
||||||
@@ -7,12 +7,7 @@ import qs.Services
|
|||||||
|
|
||||||
Variants {
|
Variants {
|
||||||
readonly property var log: Log.scoped("BlurredWallpaperBackground")
|
readonly property var log: Log.scoped("BlurredWallpaperBackground")
|
||||||
model: {
|
model: SettingsData.getFilteredScreens("wallpaper")
|
||||||
if (SessionData.isGreeterMode) {
|
|
||||||
return Quickshell.screens;
|
|
||||||
}
|
|
||||||
return SettingsData.getFilteredScreens("wallpaper");
|
|
||||||
}
|
|
||||||
|
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: blurWallpaperWindow
|
id: blurWallpaperWindow
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ Item {
|
|||||||
smooth: true
|
smooth: true
|
||||||
cache: true
|
cache: true
|
||||||
sourceSize: root.blurTextureSize
|
sourceSize: root.blurTextureSize
|
||||||
fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SessionData.getMonitorWallpaperFillMode(root.screenName))
|
fillMode: root.getFillMode(SessionData.getMonitorWallpaperFillMode(root.screenName))
|
||||||
|
|
||||||
onStatusChanged: {
|
onStatusChanged: {
|
||||||
if (status === Image.Error) {
|
if (status === Image.Error) {
|
||||||
@@ -166,7 +166,7 @@ Item {
|
|||||||
smooth: true
|
smooth: true
|
||||||
cache: true
|
cache: true
|
||||||
sourceSize: root.blurTextureSize
|
sourceSize: root.blurTextureSize
|
||||||
fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SessionData.getMonitorWallpaperFillMode(root.screenName))
|
fillMode: root.getFillMode(SessionData.getMonitorWallpaperFillMode(root.screenName))
|
||||||
|
|
||||||
onStatusChanged: {
|
onStatusChanged: {
|
||||||
if (status === Image.Error) {
|
if (status === Image.Error) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import qs.Common
|
|||||||
import qs.Modules.Plugins
|
import qs.Modules.Plugins
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
import "../../../Common/LayoutCodes.js" as LayoutCodes
|
import "../../../DankCommon/Common/LayoutCodes.js" as LayoutCodes
|
||||||
|
|
||||||
BasePill {
|
BasePill {
|
||||||
id: root
|
id: root
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
.pragma library
|
|
||||||
|
|
||||||
function readBoolOverride(envReader, names, fallbackValue) {
|
|
||||||
for (let i = 0; i < names.length; i++) {
|
|
||||||
const name = names[i];
|
|
||||||
const raw = envReader(name);
|
|
||||||
if (raw === undefined || raw === null || raw === "")
|
|
||||||
continue;
|
|
||||||
|
|
||||||
const normalized = String(raw).trim().toLowerCase();
|
|
||||||
if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on")
|
|
||||||
return true;
|
|
||||||
if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off")
|
|
||||||
return false;
|
|
||||||
|
|
||||||
console.warn("Invalid boolean override for", name + ":", raw, "- trying next override/fallback");
|
|
||||||
}
|
|
||||||
|
|
||||||
return fallbackValue;
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import "GreetdEnv.js" as GreetdEnv
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
readonly property var log: Log.scoped("GreetdMemory")
|
|
||||||
|
|
||||||
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
|
|
||||||
readonly property string sessionConfigPath: greetCfgDir + "/session.json"
|
|
||||||
readonly property string memoryFile: greetCfgDir + "/.local/state/memory.json"
|
|
||||||
readonly property bool rememberLastSession: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], true)
|
|
||||||
readonly property bool rememberLastUser: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], true)
|
|
||||||
|
|
||||||
property string lastSessionId: ""
|
|
||||||
property string lastSessionDesktopId: ""
|
|
||||||
property string lastSessionExec: ""
|
|
||||||
property string lastSuccessfulUser: ""
|
|
||||||
property bool memoryReady: false
|
|
||||||
property bool isLightMode: false
|
|
||||||
property bool nightModeEnabled: false
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
loadMemory();
|
|
||||||
loadSessionConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadMemory() {
|
|
||||||
parseMemory(memoryFileView.text());
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadSessionConfig() {
|
|
||||||
parseSessionConfig(sessionConfigFileView.text());
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseSessionConfig(content) {
|
|
||||||
try {
|
|
||||||
if (content && content.trim()) {
|
|
||||||
const config = JSON.parse(content);
|
|
||||||
isLightMode = config.isLightMode !== undefined ? config.isLightMode : false;
|
|
||||||
nightModeEnabled = config.nightModeEnabled !== undefined ? config.nightModeEnabled : false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
log.warn("Failed to parse greeter session config:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseMemory(content) {
|
|
||||||
try {
|
|
||||||
if (!content || !content.trim())
|
|
||||||
return;
|
|
||||||
const memory = JSON.parse(content);
|
|
||||||
lastSessionId = rememberLastSession ? (memory.lastSessionId || "") : "";
|
|
||||||
lastSessionDesktopId = rememberLastSession ? (memory.lastSessionDesktopId || "") : "";
|
|
||||||
lastSessionExec = rememberLastSession ? (memory.lastSessionExec || "") : "";
|
|
||||||
lastSuccessfulUser = rememberLastUser ? (memory.lastSuccessfulUser || "") : "";
|
|
||||||
if (!rememberLastSession || !rememberLastUser)
|
|
||||||
saveMemory();
|
|
||||||
} catch (e) {
|
|
||||||
log.warn("Failed to parse greetd memory:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveMemory() {
|
|
||||||
let memory = {};
|
|
||||||
if (rememberLastSession && lastSessionId)
|
|
||||||
memory.lastSessionId = lastSessionId;
|
|
||||||
if (rememberLastSession && lastSessionDesktopId)
|
|
||||||
memory.lastSessionDesktopId = lastSessionDesktopId;
|
|
||||||
if (rememberLastUser && lastSuccessfulUser)
|
|
||||||
memory.lastSuccessfulUser = lastSuccessfulUser;
|
|
||||||
memoryFileView.setText(JSON.stringify(memory, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLastSession(id, desktopId) {
|
|
||||||
if (!rememberLastSession) {
|
|
||||||
if (lastSessionId !== "" || lastSessionDesktopId !== "" || lastSessionExec !== "") {
|
|
||||||
lastSessionId = "";
|
|
||||||
lastSessionDesktopId = "";
|
|
||||||
lastSessionExec = "";
|
|
||||||
saveMemory();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastSessionId = id || "";
|
|
||||||
lastSessionDesktopId = desktopId || "";
|
|
||||||
lastSessionExec = "";
|
|
||||||
if (!lastSessionId)
|
|
||||||
lastSessionDesktopId = "";
|
|
||||||
saveMemory();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLastSessionId(id) {
|
|
||||||
setLastSession(id, lastSessionDesktopId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLastSessionDesktopId(id) {
|
|
||||||
setLastSession(lastSessionId, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLastSessionExec(exec) {
|
|
||||||
if (!rememberLastSession) {
|
|
||||||
if (lastSessionExec !== "") {
|
|
||||||
lastSessionExec = "";
|
|
||||||
saveMemory();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (lastSessionExec !== "") {
|
|
||||||
lastSessionExec = "";
|
|
||||||
saveMemory();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLastSuccessfulUser(username) {
|
|
||||||
if (!rememberLastUser) {
|
|
||||||
if (lastSuccessfulUser !== "") {
|
|
||||||
lastSuccessfulUser = "";
|
|
||||||
saveMemory();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastSuccessfulUser = username || "";
|
|
||||||
saveMemory();
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: memoryFileView
|
|
||||||
path: root.memoryFile
|
|
||||||
blockLoading: false
|
|
||||||
blockWrites: false
|
|
||||||
atomicWrites: true
|
|
||||||
watchChanges: false
|
|
||||||
printErrors: false
|
|
||||||
onLoaded: {
|
|
||||||
parseMemory(memoryFileView.text());
|
|
||||||
root.memoryReady = true;
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
root.memoryReady = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: sessionConfigFileView
|
|
||||||
path: root.sessionConfigPath
|
|
||||||
blockLoading: false
|
|
||||||
blockWrites: true
|
|
||||||
atomicWrites: false
|
|
||||||
watchChanges: false
|
|
||||||
printErrors: false
|
|
||||||
onLoaded: {
|
|
||||||
parseSessionConfig(sessionConfigFileView.text());
|
|
||||||
}
|
|
||||||
onLoadFailed: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import "GreetdEnv.js" as GreetdEnv
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
readonly property var log: Log.scoped("GreetdSettings")
|
|
||||||
|
|
||||||
readonly property string _greeterCacheDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
|
|
||||||
|
|
||||||
property string configBaseDir: root._greeterCacheDir
|
|
||||||
readonly property string configPath: root.configBaseDir ? (root.configBaseDir + "/settings.json") : ""
|
|
||||||
readonly property string greeterWallpaperOverridePath: root.configBaseDir ? (root.configBaseDir + "/greeter_wallpaper_override.jpg") : ""
|
|
||||||
|
|
||||||
function setConfigBaseDir(dir) {
|
|
||||||
const next = dir || root._greeterCacheDir;
|
|
||||||
if (configBaseDir === next)
|
|
||||||
return;
|
|
||||||
configBaseDir = next;
|
|
||||||
settingsLoaded = false;
|
|
||||||
settingsFile.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetConfigBaseDir() {
|
|
||||||
setConfigBaseDir(root._greeterCacheDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
property string currentThemeName: "purple"
|
|
||||||
property bool settingsLoaded: false
|
|
||||||
property string customThemeFile: ""
|
|
||||||
property var registryThemeVariants: ({})
|
|
||||||
property string matugenScheme: "scheme-tonal-spot"
|
|
||||||
property string clockFormat: "auto"
|
|
||||||
readonly property bool localeUses24Hour: {
|
|
||||||
const fmt = Qt.locale().timeFormat(Locale.ShortFormat).replace(/'[^']*'/g, "");
|
|
||||||
return !/[aA]/.test(fmt);
|
|
||||||
}
|
|
||||||
readonly property bool use24HourClock: clockFormat === "24h" ? true : (clockFormat === "12h" ? false : localeUses24Hour)
|
|
||||||
property bool showSeconds: false
|
|
||||||
property bool padHours12Hour: false
|
|
||||||
property string greeterLockDateFormat: ""
|
|
||||||
property string greeterFontFamily: ""
|
|
||||||
property string greeterWallpaperFillMode: ""
|
|
||||||
property bool useFahrenheit: false
|
|
||||||
property bool nightModeEnabled: false
|
|
||||||
property string weatherLocation: "New York, NY"
|
|
||||||
property string weatherCoordinates: "40.7128,-74.0060"
|
|
||||||
property bool useAutoLocation: false
|
|
||||||
property bool weatherEnabled: true
|
|
||||||
property string iconTheme: "System Default"
|
|
||||||
property bool useOSLogo: false
|
|
||||||
property string osLogoColorOverride: ""
|
|
||||||
property real osLogoBrightness: 0.5
|
|
||||||
property real osLogoContrast: 1
|
|
||||||
property string fontFamily: "Inter Variable"
|
|
||||||
property string monoFontFamily: "Fira Code"
|
|
||||||
property int fontWeight: Font.Normal
|
|
||||||
property real fontScale: 1.0
|
|
||||||
property real cornerRadius: 12
|
|
||||||
property string widgetBackgroundColor: "sch"
|
|
||||||
property string lockDateFormat: ""
|
|
||||||
property bool lockScreenShowPowerActions: true
|
|
||||||
property bool lockScreenShowProfileImage: true
|
|
||||||
property bool rememberLastSession: true
|
|
||||||
property bool rememberLastUser: true
|
|
||||||
property bool greeterAutoLogin: false
|
|
||||||
property bool greeterEnableFprint: false
|
|
||||||
property bool greeterEnableU2f: false
|
|
||||||
property string greeterWallpaperPath: ""
|
|
||||||
property bool powerActionConfirm: true
|
|
||||||
property real powerActionHoldDuration: 0.5
|
|
||||||
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
|
|
||||||
property string powerMenuDefaultAction: "logout"
|
|
||||||
property bool powerMenuGridLayout: false
|
|
||||||
property var screenPreferences: ({})
|
|
||||||
property int animationSpeed: 2
|
|
||||||
property string wallpaperFillMode: "Fill"
|
|
||||||
property string wallpaperBackgroundColorMode: "black"
|
|
||||||
property string wallpaperBackgroundCustomColor: "#000000"
|
|
||||||
readonly property color effectiveWallpaperBackgroundColor: {
|
|
||||||
switch (wallpaperBackgroundColorMode) {
|
|
||||||
case "black":
|
|
||||||
return "#000000";
|
|
||||||
case "white":
|
|
||||||
return "#ffffff";
|
|
||||||
case "primary":
|
|
||||||
return (typeof Theme !== "undefined") ? Theme.primary : "#000000";
|
|
||||||
case "surface":
|
|
||||||
return (typeof Theme !== "undefined") ? Theme.surfaceContainer : "#000000";
|
|
||||||
case "custom":
|
|
||||||
return wallpaperBackgroundCustomColor;
|
|
||||||
default:
|
|
||||||
return "#000000";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseSettings(content) {
|
|
||||||
try {
|
|
||||||
let settings = {};
|
|
||||||
if (content && content.trim()) {
|
|
||||||
settings = JSON.parse(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
const envRememberLastSession = GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], undefined);
|
|
||||||
const envRememberLastUser = GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], undefined);
|
|
||||||
|
|
||||||
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "purple";
|
|
||||||
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "";
|
|
||||||
registryThemeVariants = settings.registryThemeVariants !== undefined ? settings.registryThemeVariants : ({});
|
|
||||||
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot";
|
|
||||||
clockFormat = settings.clockFormat !== undefined ? settings.clockFormat : (settings.use24HourClock !== undefined ? (settings.use24HourClock ? "24h" : "12h") : "auto");
|
|
||||||
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
|
|
||||||
padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : false;
|
|
||||||
greeterLockDateFormat = settings.greeterLockDateFormat !== undefined ? settings.greeterLockDateFormat : "";
|
|
||||||
greeterFontFamily = settings.greeterFontFamily !== undefined ? settings.greeterFontFamily : "";
|
|
||||||
greeterWallpaperFillMode = settings.greeterWallpaperFillMode !== undefined ? settings.greeterWallpaperFillMode : "";
|
|
||||||
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
|
|
||||||
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
|
|
||||||
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
|
|
||||||
weatherCoordinates = settings.weatherCoordinates !== undefined ? settings.weatherCoordinates : "40.7128,-74.0060";
|
|
||||||
useAutoLocation = settings.useAutoLocation !== undefined ? settings.useAutoLocation : false;
|
|
||||||
weatherEnabled = settings.weatherEnabled !== undefined ? settings.weatherEnabled : true;
|
|
||||||
iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default";
|
|
||||||
useOSLogo = settings.useOSLogo !== undefined ? settings.useOSLogo : false;
|
|
||||||
osLogoColorOverride = settings.osLogoColorOverride !== undefined ? settings.osLogoColorOverride : "";
|
|
||||||
osLogoBrightness = settings.osLogoBrightness !== undefined ? settings.osLogoBrightness : 0.5;
|
|
||||||
osLogoContrast = settings.osLogoContrast !== undefined ? settings.osLogoContrast : 1;
|
|
||||||
fontFamily = settings.fontFamily !== undefined ? settings.fontFamily : Theme.defaultFontFamily;
|
|
||||||
monoFontFamily = settings.monoFontFamily !== undefined ? settings.monoFontFamily : Theme.defaultMonoFontFamily;
|
|
||||||
fontWeight = settings.fontWeight !== undefined ? settings.fontWeight : Font.Normal;
|
|
||||||
fontScale = settings.fontScale !== undefined ? settings.fontScale : 1.0;
|
|
||||||
cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12;
|
|
||||||
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch";
|
|
||||||
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : "";
|
|
||||||
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true;
|
|
||||||
lockScreenShowProfileImage = settings.lockScreenShowProfileImage !== undefined ? settings.lockScreenShowProfileImage : true;
|
|
||||||
if (envRememberLastSession !== undefined) {
|
|
||||||
rememberLastSession = envRememberLastSession;
|
|
||||||
} else {
|
|
||||||
rememberLastSession = settings.greeterRememberLastSession !== undefined ? settings.greeterRememberLastSession : settings.rememberLastSession !== undefined ? settings.rememberLastSession : true;
|
|
||||||
}
|
|
||||||
if (envRememberLastUser !== undefined) {
|
|
||||||
rememberLastUser = envRememberLastUser;
|
|
||||||
} else {
|
|
||||||
rememberLastUser = settings.greeterRememberLastUser !== undefined ? settings.greeterRememberLastUser : settings.rememberLastUser !== undefined ? settings.rememberLastUser : true;
|
|
||||||
}
|
|
||||||
if (configBaseDir === root._greeterCacheDir) {
|
|
||||||
greeterAutoLogin = settings.greeterAutoLogin !== undefined ? settings.greeterAutoLogin : false;
|
|
||||||
}
|
|
||||||
greeterEnableFprint = settings.greeterEnableFprint !== undefined ? settings.greeterEnableFprint : false;
|
|
||||||
greeterEnableU2f = settings.greeterEnableU2f !== undefined ? settings.greeterEnableU2f : false;
|
|
||||||
greeterWallpaperPath = settings.greeterWallpaperPath !== undefined ? settings.greeterWallpaperPath : "";
|
|
||||||
powerActionConfirm = settings.powerActionConfirm !== undefined ? settings.powerActionConfirm : true;
|
|
||||||
powerActionHoldDuration = settings.powerActionHoldDuration !== undefined ? settings.powerActionHoldDuration : 0.5;
|
|
||||||
powerMenuActions = settings.powerMenuActions !== undefined ? settings.powerMenuActions : ["reboot", "logout", "poweroff", "lock", "suspend", "restart"];
|
|
||||||
powerMenuDefaultAction = settings.powerMenuDefaultAction !== undefined ? settings.powerMenuDefaultAction : "logout";
|
|
||||||
powerMenuGridLayout = settings.powerMenuGridLayout !== undefined ? settings.powerMenuGridLayout : false;
|
|
||||||
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({});
|
|
||||||
animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : 2;
|
|
||||||
wallpaperFillMode = settings.wallpaperFillMode !== undefined ? settings.wallpaperFillMode : "Fill";
|
|
||||||
wallpaperBackgroundColorMode = settings.wallpaperBackgroundColorMode !== undefined ? settings.wallpaperBackgroundColorMode : "black";
|
|
||||||
wallpaperBackgroundCustomColor = settings.wallpaperBackgroundCustomColor !== undefined ? settings.wallpaperBackgroundCustomColor : "#000000";
|
|
||||||
|
|
||||||
if (typeof Theme !== "undefined") {
|
|
||||||
if (currentThemeName === "custom" && customThemeFile) {
|
|
||||||
Theme.loadCustomThemeFromFile(customThemeFile);
|
|
||||||
}
|
|
||||||
Theme.applyGreeterTheme(currentThemeName);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
log.warn("Failed to parse greetd settings:", e);
|
|
||||||
} finally {
|
|
||||||
settingsLoaded = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEffectiveTimeFormat() {
|
|
||||||
const use24 = use24HourClock;
|
|
||||||
const secs = showSeconds;
|
|
||||||
const pad = padHours12Hour;
|
|
||||||
if (use24)
|
|
||||||
return secs ? "hh:mm:ss" : "hh:mm";
|
|
||||||
if (pad)
|
|
||||||
return secs ? "hh:mm:ss AP" : "hh:mm AP";
|
|
||||||
return secs ? "h:mm:ss AP" : "h:mm AP";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEffectiveLockDateFormat() {
|
|
||||||
const fmt = (greeterLockDateFormat !== undefined && greeterLockDateFormat !== "") ? greeterLockDateFormat : lockDateFormat;
|
|
||||||
return fmt && fmt.length > 0 ? fmt : Locale.LongFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEffectiveWallpaperFillMode() {
|
|
||||||
return (greeterWallpaperFillMode && greeterWallpaperFillMode !== "") ? greeterWallpaperFillMode : wallpaperFillMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEffectiveFontFamily() {
|
|
||||||
return (greeterFontFamily && greeterFontFamily !== "") ? greeterFontFamily : fontFamily;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFilteredScreens(componentId) {
|
|
||||||
const prefs = screenPreferences && screenPreferences[componentId] || ["all"];
|
|
||||||
if (prefs.includes("all")) {
|
|
||||||
return Quickshell.screens;
|
|
||||||
}
|
|
||||||
return Quickshell.screens.filter(screen => prefs.includes(screen.name));
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: settingsFile
|
|
||||||
path: root.configPath
|
|
||||||
blockLoading: false
|
|
||||||
blockWrites: true
|
|
||||||
atomicWrites: false
|
|
||||||
watchChanges: false
|
|
||||||
printErrors: false
|
|
||||||
onLoaded: {
|
|
||||||
parseSettings(settingsFile.text());
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
root.parseSettings("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,35 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string passwordBuffer: ""
|
|
||||||
property string username: ""
|
|
||||||
property string usernameInput: ""
|
|
||||||
property bool showPasswordInput: false
|
|
||||||
property string selectedSession: ""
|
|
||||||
property string selectedSessionPath: ""
|
|
||||||
property string selectedSessionDesktopId: ""
|
|
||||||
property string pamState: ""
|
|
||||||
property bool unlocking: false
|
|
||||||
|
|
||||||
property var sessionList: []
|
|
||||||
property var sessionExecs: []
|
|
||||||
property var sessionPaths: []
|
|
||||||
property var sessionDesktopIds: []
|
|
||||||
property int currentSessionIndex: 0
|
|
||||||
property var availableUsers: []
|
|
||||||
property int selectedUserIndex: -1
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
showPasswordInput = false;
|
|
||||||
username = "";
|
|
||||||
usernameInput = "";
|
|
||||||
passwordBuffer = "";
|
|
||||||
pamState = "";
|
|
||||||
selectedUserIndex = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Services.Greetd
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Variants {
|
|
||||||
model: Quickshell.screens
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var modelData
|
|
||||||
|
|
||||||
screen: modelData
|
|
||||||
anchors {
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
top: true
|
|
||||||
bottom: true
|
|
||||||
}
|
|
||||||
exclusionMode: ExclusionMode.Normal
|
|
||||||
WlrLayershell.layer: WlrLayer.Overlay
|
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
GreeterContent {
|
|
||||||
anchors.fill: parent
|
|
||||||
screenName: root.screen?.name ?? ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
LayoutMirroring.enabled: I18n.isRtl
|
|
||||||
LayoutMirroring.childrenInherit: true
|
|
||||||
|
|
||||||
property bool expanded: false
|
|
||||||
property int maxExpandedHeight: 400
|
|
||||||
property bool autoLoginVisible: false
|
|
||||||
property bool autoLoginChecked: false
|
|
||||||
property bool manualEntryVisible: false
|
|
||||||
|
|
||||||
signal userSelected(string username)
|
|
||||||
signal toggleRequested
|
|
||||||
signal autoLoginToggled
|
|
||||||
signal manualEntryRequested
|
|
||||||
|
|
||||||
readonly property int rowHeight: 52
|
|
||||||
readonly property int collapsedBarHeight: 36
|
|
||||||
readonly property int actionRowHeight: 44
|
|
||||||
|
|
||||||
readonly property int userListFullHeight: {
|
|
||||||
const count = GreeterUsersService.users.length;
|
|
||||||
if (count === 0)
|
|
||||||
return 0;
|
|
||||||
return count * rowHeight + Math.max(0, count - 1) * Theme.spacingXS;
|
|
||||||
}
|
|
||||||
readonly property int manualEntryBlockHeight: manualEntryVisible ? actionRowHeight + Theme.spacingXS : 0
|
|
||||||
readonly property int autoLoginBlockHeight: autoLoginVisible ? actionRowHeight + Theme.spacingXS : 0
|
|
||||||
readonly property int expandedContentHeight: {
|
|
||||||
if (!expanded)
|
|
||||||
return 0;
|
|
||||||
if (GreeterUsersService.users.length === 0 && !autoLoginVisible && !manualEntryVisible)
|
|
||||||
return 0;
|
|
||||||
return Math.min(maxExpandedHeight, userListFullHeight + manualEntryBlockHeight + autoLoginBlockHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
function encodeFileUrl(path) {
|
|
||||||
if (!path)
|
|
||||||
return "";
|
|
||||||
return "file://" + path.split("/").map(s => encodeURIComponent(s)).join("/");
|
|
||||||
}
|
|
||||||
|
|
||||||
function profileImageSource(username) {
|
|
||||||
const path = GreeterUsersService.profileImagePath(username);
|
|
||||||
if (path)
|
|
||||||
return encodeFileUrl(path);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
implicitHeight: expanded ? expandedContentHeight : collapsedBarHeight
|
|
||||||
implicitWidth: parent ? parent.width : 320
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
height: collapsedBarHeight
|
|
||||||
visible: !expanded
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: GreeterState.username ? GreeterUsersService.optionLabel(GreeterState.username) : I18n.tr("Select user...", "greeter user picker placeholder")
|
|
||||||
color: GreeterState.username ? Theme.surfaceText : Theme.surfaceVariantText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
name: "expand_more"
|
|
||||||
size: 20
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: root.toggleRequested()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
height: root.expandedContentHeight
|
|
||||||
visible: expanded
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankListView {
|
|
||||||
id: userListView
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - root.manualEntryBlockHeight - root.autoLoginBlockHeight
|
|
||||||
clip: true
|
|
||||||
interactive: contentHeight > height
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
model: GreeterUsersService.users
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
id: userRow
|
|
||||||
|
|
||||||
required property var modelData
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
width: userListView.width
|
|
||||||
height: root.rowHeight
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: userRowMouse.containsMouse ? Theme.surfacePressed : Theme.withAlpha(Theme.surfacePressed, 0)
|
|
||||||
border.color: GreeterState.username === userRow.modelData.username ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
|
||||||
border.width: GreeterState.username === userRow.modelData.username ? 1 : 0
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.preferredWidth: 36
|
|
||||||
Layout.preferredHeight: 36
|
|
||||||
|
|
||||||
DankCircularImage {
|
|
||||||
anchors.fill: parent
|
|
||||||
imageSource: root.profileImageSource(userRow.modelData.username)
|
|
||||||
fallbackIcon: "person"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: GreeterUsersService.optionLabel(userRow.modelData.username)
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: userRowMouse
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: root.userSelected(userRow.modelData.username)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: root.actionRowHeight
|
|
||||||
visible: root.manualEntryVisible
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: manualEntryRowMouse.containsMouse ? Theme.surfacePressed : Theme.withAlpha(Theme.surfacePressed, 0)
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
name: "person_add"
|
|
||||||
size: 20
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: I18n.tr("Not listed?", "greeter link to switch to manual username entry")
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: manualEntryRowMouse
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: root.manualEntryRequested()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: root.actionRowHeight
|
|
||||||
visible: root.autoLoginVisible
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: autoLoginRowMouse.containsMouse ? Theme.surfacePressed : Theme.withAlpha(Theme.surfacePressed, 0)
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
name: root.autoLoginChecked ? "check_box" : "check_box_outline_blank"
|
|
||||||
size: 20
|
|
||||||
color: root.autoLoginChecked ? Theme.primary : Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: I18n.tr("Auto-login")
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: autoLoginRowMouse
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: root.autoLoginToggled()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property var log: Log.scoped("GreeterUserTheme")
|
|
||||||
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
|
|
||||||
|
|
||||||
property string activeUsername: ""
|
|
||||||
|
|
||||||
function userCacheDir(username) {
|
|
||||||
if (!username)
|
|
||||||
return "";
|
|
||||||
return greetCfgDir + "/users/" + username;
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyForUser(username) {
|
|
||||||
const name = (username || "").trim();
|
|
||||||
activeUsername = name;
|
|
||||||
if (!name) {
|
|
||||||
applyDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const dir = userCacheDir(name);
|
|
||||||
if (typeof GreeterUsersService !== "undefined" && GreeterUsersService.hasSyncedTheme(name)) {
|
|
||||||
Theme.setGreeterColorsBaseDir(dir);
|
|
||||||
SessionData.setGreeterSessionBaseDir(dir);
|
|
||||||
GreetdSettings.setConfigBaseDir(dir);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
applyDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyDefault() {
|
|
||||||
activeUsername = "";
|
|
||||||
Theme.resetGreeterColorsBaseDir();
|
|
||||||
SessionData.resetGreeterSessionBaseDir();
|
|
||||||
GreetdSettings.resetConfigBaseDir();
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property string activeWallpaperOverridePath: {
|
|
||||||
const base = activeUsername && typeof GreeterUsersService !== "undefined" && GreeterUsersService.hasSyncedTheme(activeUsername) ? userCacheDir(activeUsername) : greetCfgDir;
|
|
||||||
return base ? base + "/greeter_wallpaper_override.jpg" : "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,320 +0,0 @@
|
|||||||
# Dank (dms) Greeter
|
|
||||||
|
|
||||||
A greeter for [greetd](https://github.com/kennylevinsen/greetd) that follows the aesthetics of the dms lock screen.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **Multi user**: Login with any system user
|
|
||||||
- **dms sync**: Sync settings with dms for consistent styling between shell and greeter
|
|
||||||
- **Multiple compositors**: The `dms-greeter` wrapper supports niri, Hyprland, sway, scroll, miracle-wm, labwc, and mangowc.
|
|
||||||
- **Custom PAM**: Supports custom PAM configuration in `/etc/pam.d/greetd`
|
|
||||||
- **Session Memory**: Remembers last selected session and user
|
|
||||||
- Can be disabled via `settings.json` keys: `greeterRememberLastSession` and `greeterRememberLastUser`
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Arch Linux
|
|
||||||
|
|
||||||
Arch linux users can install [greetd-dms-greeter-git](https://aur.archlinux.org/packages/greetd-dms-greeter-git) from the AUR.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
paru -S greetd-dms-greeter-git
|
|
||||||
# Or with yay
|
|
||||||
yay -S greetd-dms-greeter-git
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debian / openSUSE
|
|
||||||
|
|
||||||
Official packages are available from the [DankLinux OBS repository](https://software.opensuse.org/download/package?package=dms-greeter&project=home%3AAvengeMedia%3Adanklinux). Add the repo for your distribution and install:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Debian 13
|
|
||||||
sudo apt install dms-greeter # after adding the repo
|
|
||||||
|
|
||||||
# openSUSE Tumbleweed
|
|
||||||
zypper install dms-greeter # after adding the repo
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [Installation guide](https://danklinux.com/docs/dankgreeter/installation) for full repository setup.
|
|
||||||
|
|
||||||
If you previously installed manually, remove legacy files first:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo rm -f /usr/local/bin/dms-greeter
|
|
||||||
sudo rm -rf /etc/xdg/quickshell/dms-greeter
|
|
||||||
```
|
|
||||||
|
|
||||||
Then complete setup:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dms greeter enable
|
|
||||||
dms greeter sync
|
|
||||||
```
|
|
||||||
|
|
||||||
Fingerprint authentication is optional. Install your distribution's
|
|
||||||
fingerprint service/client and PAM integration before enabling it. Package
|
|
||||||
names vary by distribution; on Fedora the native stack is:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo dnf install fprintd fprintd-pam
|
|
||||||
fprintd-enroll
|
|
||||||
```
|
|
||||||
|
|
||||||
The distro `fprintd` package normally pulls in `libfprint` automatically. After
|
|
||||||
enrollment, enable fingerprint login in **Settings → Greeter**. The toggle
|
|
||||||
applies the shared PAM configuration automatically, so no additional sync is
|
|
||||||
needed after initial greeter setup. Use `dms auth sync` only to repair or apply
|
|
||||||
authentication manually without syncing the rest of the greeter.
|
|
||||||
|
|
||||||
greetd tries fingerprint and password sequentially. DMS-managed PAM allows two
|
|
||||||
scans for up to ten seconds before password fallback. Existing distro PAM
|
|
||||||
policy takes precedence, so fallback timing can differ.
|
|
||||||
|
|
||||||
Verify enrollment with `fprintd-list "$USER"`; pass a token such as
|
|
||||||
`fprintd-enroll -f right-thumb` to enroll a specific finger. Security keys
|
|
||||||
similarly require `pam_u2f` setup and key registration before enabling the
|
|
||||||
login toggle.
|
|
||||||
|
|
||||||
If `fprintd-enroll` reports **No devices available**, check the [libfprint
|
|
||||||
supported devices list](https://fprint.freedesktop.org/supported-devices.html).
|
|
||||||
Some Validity/Synaptics readers are not supported by the native stack,
|
|
||||||
regardless of distribution; the
|
|
||||||
[open-fprintd](https://github.com/uunicorn/open-fprintd) service with the
|
|
||||||
[python-validity](https://github.com/uunicorn/python-validity) driver may work
|
|
||||||
instead. Follow the driver's installation and firmware instructions, and do
|
|
||||||
not run the stock `fprintd` daemon alongside its replacement.
|
|
||||||
|
|
||||||
#### Syncing themes (Optional)
|
|
||||||
|
|
||||||
To sync your wallpaper and theme with the greeter login screen, follow the manual setup below:
|
|
||||||
|
|
||||||
##### Manual theme syncing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Add yourself to greeter group
|
|
||||||
sudo usermod -aG greeter <username>
|
|
||||||
|
|
||||||
# Set ACLs to allow greeter to traverse your directories
|
|
||||||
setfacl -m u:greeter:x ~ ~/.config ~/.local ~/.cache ~/.local/state
|
|
||||||
|
|
||||||
# Set group ownership on config directories
|
|
||||||
sudo chgrp -R greeter ~/.config/DankMaterialShell
|
|
||||||
sudo chgrp -R greeter ~/.local/state/DankMaterialShell
|
|
||||||
sudo chgrp -R greeter ~/.cache/DankMaterialShell
|
|
||||||
sudo chmod -R g+rX ~/.config/DankMaterialShell ~/.cache/DankMaterialShell ~/.cache/quickshell
|
|
||||||
|
|
||||||
# Create symlinks
|
|
||||||
sudo ln -sf ~/.config/DankMaterialShell/settings.json /var/cache/dms-greeter/settings.json
|
|
||||||
sudo ln -sf ~/.local/state/DankMaterialShell/session.json /var/cache/dms-greeter/session.json
|
|
||||||
sudo ln -sf ~/.cache/DankMaterialShell/dms-colors.json /var/cache/dms-greeter/colors.json
|
|
||||||
|
|
||||||
# Logout and login for group membership to take effect
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
### Fedora / RHEL / Rocky / Alma
|
|
||||||
|
|
||||||
Install from COPR or build the RPM:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# From COPR (when available)
|
|
||||||
sudo dnf copr enable avenge/dms
|
|
||||||
sudo dnf install dms-greeter
|
|
||||||
|
|
||||||
# Or build locally
|
|
||||||
cd /path/to/DankMaterialShell
|
|
||||||
rpkg local
|
|
||||||
sudo rpm -ivh x86_64/dms-greeter-*.rpm
|
|
||||||
```
|
|
||||||
|
|
||||||
The package automatically:
|
|
||||||
|
|
||||||
- Creates the greeter user (via `systemd-sysusers` from `/usr/lib/sysusers.d/dms-greeter.conf` for atomic/immutable compatibility, with package script fallback)
|
|
||||||
- Sets up directories and permissions
|
|
||||||
- Configures greetd with auto-detected compositor
|
|
||||||
- Applies SELinux contexts
|
|
||||||
|
|
||||||
Then complete setup:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dms greeter enable
|
|
||||||
dms greeter sync
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Syncing themes (Optional)
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dms greeter sync
|
|
||||||
```
|
|
||||||
|
|
||||||
Then logout/login to see your wallpaper on the greeter.
|
|
||||||
|
|
||||||
### Automatic
|
|
||||||
|
|
||||||
The easiest thing is to run `dms greeter install` or `dms` for interactive installation.
|
|
||||||
On Debian/openSUSE, this now prefers the `dms-greeter` package when the OBS repo is configured.
|
|
||||||
|
|
||||||
### Manual (fallback only)
|
|
||||||
|
|
||||||
Use this only if no package is available for your distro.
|
|
||||||
|
|
||||||
1. Install `greetd` (in most distro's standard repositories) and `quickshell`
|
|
||||||
|
|
||||||
2. Create the greeter user (if not already created by greetd):
|
|
||||||
```bash
|
|
||||||
sudo groupadd -r greeter
|
|
||||||
sudo useradd -r -g greeter -d /var/lib/greeter -s /bin/bash -c "System Greeter" greeter
|
|
||||||
sudo mkdir -p /var/lib/greeter
|
|
||||||
sudo chown greeter:greeter /var/lib/greeter
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Clone the dms project to `/etc/xdg/quickshell/dms-greeter`:
|
|
||||||
```bash
|
|
||||||
sudo git clone https://github.com/AvengeMedia/DankMaterialShell.git /etc/xdg/quickshell/dms-greeter
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Copy `Modules/Greetd/assets/dms-greeter` to `/usr/local/bin/dms-greeter`:
|
|
||||||
```bash
|
|
||||||
sudo cp /etc/xdg/quickshell/dms-greeter/Modules/Greetd/assets/dms-greeter /usr/local/bin/dms-greeter
|
|
||||||
sudo chmod +x /usr/local/bin/dms-greeter
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Create greeter cache directory with proper permissions:
|
|
||||||
```bash
|
|
||||||
sudo mkdir -p /var/cache/dms-greeter
|
|
||||||
sudo chown <greeter-user>:<greeter-group> /var/cache/dms-greeter
|
|
||||||
sudo chmod 2770 /var/cache/dms-greeter
|
|
||||||
```
|
|
||||||
|
|
||||||
6. Edit or create `/etc/greetd/config.toml`:
|
|
||||||
```toml
|
|
||||||
[terminal]
|
|
||||||
vt = 1
|
|
||||||
|
|
||||||
[default_session]
|
|
||||||
user = "greeter"
|
|
||||||
# Change compositor to another wrapper-supported compositor if preferred
|
|
||||||
command = "/usr/local/bin/dms-greeter --command niri"
|
|
||||||
```
|
|
||||||
|
|
||||||
7. Disable existing display manager and enable greetd:
|
|
||||||
```bash
|
|
||||||
sudo systemctl disable gdm sddm lightdm
|
|
||||||
sudo systemctl enable greetd
|
|
||||||
```
|
|
||||||
|
|
||||||
8. (Optional) Set up theme syncing using the manual ACL method described in the Configuration → Personalization section below
|
|
||||||
|
|
||||||
#### Legacy installation (deprecated)
|
|
||||||
|
|
||||||
If you prefer the old method with separate shell scripts and config files:
|
|
||||||
1. Copy `assets/dms-niri.kdl` or `assets/dms-hypr.lua` (legacy: `assets/dms-hypr.conf`) to `/etc/greetd`
|
|
||||||
2. Copy `assets/greet-niri.sh` or `assets/greet-hyprland.sh` to `/usr/local/bin/start-dms-greetd.sh`
|
|
||||||
3. Edit the config file and replace `_DMS_PATH_` with your DMS installation path
|
|
||||||
4. Configure greetd to use `/usr/local/bin/start-dms-greetd.sh`
|
|
||||||
|
|
||||||
### NixOS
|
|
||||||
|
|
||||||
To install the greeter on NixOS add the repo to your flake inputs as described in the readme. Then somewhere in your NixOS config add this to imports:
|
|
||||||
```nix
|
|
||||||
imports = [
|
|
||||||
inputs.dank-material-shell.nixosModules.greeter
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Enable the greeter with this in your NixOS config:
|
|
||||||
```nix
|
|
||||||
programs.dank-material-shell.greeter = {
|
|
||||||
enable = true;
|
|
||||||
compositor.name = "niri"; # or set to hyprland
|
|
||||||
configHome = "/home/user"; # optionally copyies that users DMS settings (and wallpaper if set) to the greeters data directory as root before greeter starts
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Using dms-greeter wrapper (recommended)
|
|
||||||
|
|
||||||
The `dms-greeter` wrapper simplifies running the greeter with any compositor:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dms-greeter --command niri
|
|
||||||
dms-greeter --command hyprland
|
|
||||||
dms-greeter --command sway
|
|
||||||
dms-greeter --command mangowc
|
|
||||||
dms-greeter --command niri -C /path/to/custom-niri.kdl
|
|
||||||
dms-greeter --command niri --remember-last-user false --remember-last-session false
|
|
||||||
```
|
|
||||||
|
|
||||||
Configure greetd to use it in `/etc/greetd/config.toml`:
|
|
||||||
```toml
|
|
||||||
[terminal]
|
|
||||||
vt = 1
|
|
||||||
|
|
||||||
[default_session]
|
|
||||||
user = "greeter"
|
|
||||||
command = "/usr/bin/dms-greeter --command niri"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manual usage
|
|
||||||
|
|
||||||
To run dms in greeter mode you can also manually set environment variables:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
DMS_RUN_GREETER=1 qs -p /path/to/dms
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
#### Compositor
|
|
||||||
|
|
||||||
For current wrapper-based installs, the `dms-greeter` wrapper supports niri, hyprland, sway, scroll, miracle-wm, labwc, and mangowc.
|
|
||||||
|
|
||||||
Only niri currently has a generated greeter config path managed by `dms greeter sync`.
|
|
||||||
|
|
||||||
- niri: `dms greeter sync` writes the generated greeter config to `/etc/greetd/niri/config.kdl`. Add local manual tweaks in `/etc/greetd/niri_overrides.kdl`.
|
|
||||||
- Other wrapper-supported compositors use the wrapper-generated config by default. If you need a custom compositor config, add `-C /path/to/config` to the `dms-greeter` command in `/etc/greetd/config.toml`.
|
|
||||||
|
|
||||||
#### Personalization
|
|
||||||
|
|
||||||
The greeter can be personalized with wallpapers, themes, weather, clock formats, and more - configured exactly the same as dms.
|
|
||||||
|
|
||||||
**Easiest method (single user):** Run `dms greeter sync` to automatically sync your DMS theme with the greeter.
|
|
||||||
|
|
||||||
**Multi-user systems:** One **main admin** runs full sync once to set up greetd and the shared cache (`dms greeter sync`, or `dms greeter sync --local` when developing from a checkout). **Every other account**—including other admins—should only run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dms greeter sync --profile
|
|
||||||
```
|
|
||||||
|
|
||||||
Before that, an administrator must add each user to the `greeter` group in **Settings → Users** (greeter toggle) or with `sudo usermod -aG greeter <username>`. Each added user must log out and back in before `--profile` will work.
|
|
||||||
|
|
||||||
Per-user settings are stored under `/var/cache/dms-greeter/users/<username>/` for the login picker; the root cache remains the default fallback and is owned by whoever ran full sync.
|
|
||||||
|
|
||||||
**Manual method:** You can manually synchronize configurations if you want greeter settings to always mirror your shell:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Add yourself to the greeter group
|
|
||||||
sudo usermod -aG greeter $USER
|
|
||||||
|
|
||||||
# Set ACLs to allow greeter user to traverse your home directory
|
|
||||||
setfacl -m u:greeter:x ~ ~/.config ~/.local ~/.cache ~/.local/state
|
|
||||||
|
|
||||||
# Set group permissions on DMS directories
|
|
||||||
sudo chgrp -R greeter ~/.config/DankMaterialShell ~/.local/state/DankMaterialShell ~/.cache/quickshell
|
|
||||||
sudo chmod -R g+rX ~/.config/DankMaterialShell ~/.local/state/DankMaterialShell ~/.cache/quickshell
|
|
||||||
|
|
||||||
# Create symlinks for theme files
|
|
||||||
sudo ln -sf ~/.config/DankMaterialShell/settings.json /var/cache/dms-greeter/settings.json
|
|
||||||
sudo ln -sf ~/.local/state/DankMaterialShell/session.json /var/cache/dms-greeter/session.json
|
|
||||||
sudo ln -sf ~/.cache/DankMaterialShell/dms-colors.json /var/cache/dms-greeter/colors.json
|
|
||||||
|
|
||||||
# Logout and login for group membership to take effect
|
|
||||||
```
|
|
||||||
|
|
||||||
**Advanced:** You can override the configuration path with the `DMS_GREET_CFG_DIR` environment variable or the `--cache-dir` flag when using `dms-greeter`. The default is `/var/cache/dms-greeter`.
|
|
||||||
|
|
||||||
The cache directory should be owned by `<greeter-user>:<greeter-group>` with `2770` permissions. If the greeter user is not available yet, DMS falls back to `root:<greeter-group>`.
|
|
||||||
@@ -1,499 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
COMPOSITOR=""
|
|
||||||
COMPOSITOR_CONFIG=""
|
|
||||||
DMS_PATH="dms-greeter"
|
|
||||||
CACHE_DIR="/var/cache/dms-greeter"
|
|
||||||
REMEMBER_LAST_SESSION=""
|
|
||||||
REMEMBER_LAST_USER=""
|
|
||||||
DEBUG_MODE=0
|
|
||||||
|
|
||||||
show_help() {
|
|
||||||
cat << EOF
|
|
||||||
dms-greeter - DankMaterialShell greeter launcher
|
|
||||||
|
|
||||||
Usage: dms-greeter --command COMPOSITOR [OPTIONS]
|
|
||||||
|
|
||||||
Required:
|
|
||||||
--command COMPOSITOR Compositor to use (niri, hyprland, sway, scroll, miracle, mango, or labwc)
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-C, --config PATH Custom compositor config file
|
|
||||||
-p, --path PATH DMS path (config name or absolute path)
|
|
||||||
(default: dms-greeter)
|
|
||||||
--cache-dir PATH Cache directory for greeter data
|
|
||||||
(default: /var/cache/dms-greeter)
|
|
||||||
--remember-last-session BOOL
|
|
||||||
Persist selected session to greeter memory
|
|
||||||
(BOOL: true/false, default: from settings.json)
|
|
||||||
--remember-last-user BOOL
|
|
||||||
Persist last successful username to greeter memory
|
|
||||||
(BOOL: true/false, default: from settings.json)
|
|
||||||
--no-save-session Alias for --remember-last-session false
|
|
||||||
--no-save-username Alias for --remember-last-user false
|
|
||||||
--debug Enable verbose startup logging to stderr
|
|
||||||
-h, --help Show this help message
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
dms-greeter --command niri
|
|
||||||
dms-greeter --command hyprland -C /etc/greetd/custom-hypr.conf
|
|
||||||
dms-greeter --command sway -p /home/user/.config/quickshell/custom-dms
|
|
||||||
dms-greeter --command scroll -p /home/user/.config/quickshell/custom-dms
|
|
||||||
dms-greeter --command niri --cache-dir /tmp/dmsgreeter
|
|
||||||
dms-greeter --command niri --no-save-session --no-save-username
|
|
||||||
dms-greeter --command mango
|
|
||||||
dms-greeter --command labwc
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
require_command() {
|
|
||||||
local cmd="$1"
|
|
||||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
||||||
echo "Error: required command '$cmd' was not found in PATH" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
normalize_bool_flag() {
|
|
||||||
local flag_name="$1"
|
|
||||||
local value="$2"
|
|
||||||
local normalized="${value,,}"
|
|
||||||
|
|
||||||
case "$normalized" in
|
|
||||||
1|true|yes|on)
|
|
||||||
echo "1"
|
|
||||||
;;
|
|
||||||
0|false|no|off)
|
|
||||||
echo "0"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Error: $flag_name must be true/false (or 1/0, yes/no, on/off)" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
is_hyprland_lua_config() {
|
|
||||||
local config_file="$1"
|
|
||||||
case "$config_file" in
|
|
||||||
*.lua)
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
if [[ -f "$config_file" ]] && grep -qE '(^|[^[:alnum:]_])hl\.' "$config_file"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
clear_vt() {
|
|
||||||
local clear_sequence=$'\033[2J\033[H\033[3J\033[?25l'
|
|
||||||
|
|
||||||
# Clear retained console text on the controlling VT.
|
|
||||||
if printf '%s' "$clear_sequence" >/dev/tty 2>/dev/null; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -t 1 ]]; then
|
|
||||||
printf '%s' "$clear_sequence"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
exec_compositor() {
|
|
||||||
local log_tag="$1"
|
|
||||||
shift
|
|
||||||
|
|
||||||
if [[ "$DEBUG_MODE" == "1" ]]; then
|
|
||||||
exec "$@"
|
|
||||||
fi
|
|
||||||
|
|
||||||
clear_vt
|
|
||||||
|
|
||||||
if command -v systemd-cat >/dev/null 2>&1; then
|
|
||||||
exec "$@" > >(systemd-cat -t "dms-greeter/$log_tag" -p info 2>/dev/null) 2>&1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Preserve diagnostics on systems without journald.
|
|
||||||
exec "$@" >>"$CACHE_DIR/$log_tag.log" 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
--command)
|
|
||||||
COMPOSITOR="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-C|--config)
|
|
||||||
COMPOSITOR_CONFIG="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-p|--path)
|
|
||||||
DMS_PATH="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--cache-dir)
|
|
||||||
CACHE_DIR="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--remember-last-session)
|
|
||||||
REMEMBER_LAST_SESSION="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--remember-last-user)
|
|
||||||
REMEMBER_LAST_USER="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--no-save-session)
|
|
||||||
REMEMBER_LAST_SESSION="0"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--no-save-username)
|
|
||||||
REMEMBER_LAST_USER="0"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--debug)
|
|
||||||
DEBUG_MODE=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
show_help
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option: $1" >&2
|
|
||||||
show_help
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -z "$COMPOSITOR" ]]; then
|
|
||||||
echo "Error: --command COMPOSITOR is required" >&2
|
|
||||||
show_help
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
locate_dms_config() {
|
|
||||||
local config_name="$1"
|
|
||||||
local search_paths=()
|
|
||||||
|
|
||||||
local config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
|
|
||||||
search_paths+=("$config_home/quickshell/$config_name")
|
|
||||||
|
|
||||||
search_paths+=("/usr/share/quickshell/$config_name")
|
|
||||||
|
|
||||||
local config_dirs="${XDG_CONFIG_DIRS:-/etc/xdg}"
|
|
||||||
IFS=':' read -ra dirs <<< "$config_dirs"
|
|
||||||
for dir in "${dirs[@]}"; do
|
|
||||||
if [[ -n "$dir" ]]; then
|
|
||||||
search_paths+=("$dir/quickshell/$config_name")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
for path in "${search_paths[@]}"; do
|
|
||||||
if [[ -f "$path/shell.qml" ]]; then
|
|
||||||
echo "$path"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
export XDG_SESSION_TYPE=wayland
|
|
||||||
export QT_QPA_PLATFORM=wayland
|
|
||||||
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
|
|
||||||
export EGL_PLATFORM=gbm
|
|
||||||
export DMS_RUN_GREETER=1
|
|
||||||
|
|
||||||
if [[ ! -d "$CACHE_DIR" ]]; then
|
|
||||||
echo "Error: cache directory '$CACHE_DIR' does not exist." >&2
|
|
||||||
echo " Run 'dms greeter sync' to initialize it, or pass --cache-dir to an existing directory." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
export DMS_GREET_CFG_DIR="$CACHE_DIR"
|
|
||||||
|
|
||||||
if [[ -n "$REMEMBER_LAST_SESSION" ]]; then
|
|
||||||
DMS_GREET_REMEMBER_LAST_SESSION=$(normalize_bool_flag "--remember-last-session" "$REMEMBER_LAST_SESSION")
|
|
||||||
export DMS_GREET_REMEMBER_LAST_SESSION
|
|
||||||
if [[ "$DMS_GREET_REMEMBER_LAST_SESSION" == "1" ]]; then
|
|
||||||
DMS_SAVE_SESSION=true
|
|
||||||
else
|
|
||||||
DMS_SAVE_SESSION=false
|
|
||||||
fi
|
|
||||||
export DMS_SAVE_SESSION
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "$REMEMBER_LAST_USER" ]]; then
|
|
||||||
DMS_GREET_REMEMBER_LAST_USER=$(normalize_bool_flag "--remember-last-user" "$REMEMBER_LAST_USER")
|
|
||||||
export DMS_GREET_REMEMBER_LAST_USER
|
|
||||||
if [[ "$DMS_GREET_REMEMBER_LAST_USER" == "1" ]]; then
|
|
||||||
DMS_SAVE_USERNAME=true
|
|
||||||
else
|
|
||||||
DMS_SAVE_USERNAME=false
|
|
||||||
fi
|
|
||||||
export DMS_SAVE_USERNAME
|
|
||||||
fi
|
|
||||||
|
|
||||||
export HOME="$CACHE_DIR"
|
|
||||||
export XDG_STATE_HOME="$CACHE_DIR/.local/state"
|
|
||||||
export XDG_DATA_HOME="$CACHE_DIR/.local/share"
|
|
||||||
export XDG_CACHE_HOME="$CACHE_DIR/.cache"
|
|
||||||
|
|
||||||
# Fallback runtime dir for systems without logind/pam_rundir (e.g. Void+seatd),
|
|
||||||
# where Wayland compositors abort with "RuntimeDirNotSet". Guarded so logind
|
|
||||||
# systems keep their own.
|
|
||||||
if [[ -z "${XDG_RUNTIME_DIR:-}" || ! -d "${XDG_RUNTIME_DIR:-}" ]]; then
|
|
||||||
export XDG_RUNTIME_DIR="$CACHE_DIR/run"
|
|
||||||
mkdir -p "$XDG_RUNTIME_DIR"
|
|
||||||
chmod 700 "$XDG_RUNTIME_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
# Keep greeter VT clean by default; callers can override via env or --debug.
|
|
||||||
if [[ -z "${RUST_LOG:-}" ]]; then
|
|
||||||
export RUST_LOG=warn
|
|
||||||
fi
|
|
||||||
if [[ -z "${NIRI_LOG:-}" ]]; then
|
|
||||||
export NIRI_LOG=warn
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Export the user's configured cursor (from synced settings.json) for the
|
|
||||||
# compositor and greeter shell, but only if that theme is installed system-wide.
|
|
||||||
apply_user_cursor() {
|
|
||||||
command -v jq >/dev/null 2>&1 || return 0
|
|
||||||
local settings="$DMS_GREET_CFG_DIR/settings.json"
|
|
||||||
[[ -f "$settings" ]] || return 0
|
|
||||||
|
|
||||||
local theme size
|
|
||||||
theme=$(jq -r '.cursorSettings.theme // empty' "$settings" 2>/dev/null)
|
|
||||||
size=$(jq -r '.cursorSettings.size // empty' "$settings" 2>/dev/null)
|
|
||||||
[[ -n "$theme" && "$theme" != "System Default" ]] || return 0
|
|
||||||
|
|
||||||
local dirs=()
|
|
||||||
IFS=':' read -ra data_dirs <<< "${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
|
|
||||||
local d
|
|
||||||
for d in "${data_dirs[@]}"; do
|
|
||||||
[[ -n "$d" ]] && dirs+=("$d/icons")
|
|
||||||
done
|
|
||||||
dirs+=(/run/current-system/sw/share/icons /usr/share/icons /usr/local/share/icons)
|
|
||||||
|
|
||||||
for d in "${dirs[@]}"; do
|
|
||||||
if [[ -d "$d/$theme/cursors" ]]; then
|
|
||||||
export XCURSOR_PATH="$d${XCURSOR_PATH:+:$XCURSOR_PATH}"
|
|
||||||
export XCURSOR_THEME="$theme"
|
|
||||||
[[ -n "$size" ]] && export XCURSOR_SIZE="$size"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
apply_user_cursor
|
|
||||||
|
|
||||||
if command -v qs >/dev/null 2>&1; then
|
|
||||||
QS_BIN="qs"
|
|
||||||
elif command -v quickshell >/dev/null 2>&1; then
|
|
||||||
QS_BIN="quickshell"
|
|
||||||
else
|
|
||||||
echo "Error: neither 'qs' nor 'quickshell' was found in PATH" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
QS_CMD="$QS_BIN"
|
|
||||||
if [[ "$DMS_PATH" == /* ]]; then
|
|
||||||
QS_CMD="$QS_BIN -p $DMS_PATH"
|
|
||||||
else
|
|
||||||
RESOLVED_PATH=$(locate_dms_config "$DMS_PATH")
|
|
||||||
if [[ $? -eq 0 && -n "$RESOLVED_PATH" ]]; then
|
|
||||||
if [[ "$DEBUG_MODE" == "1" ]]; then
|
|
||||||
echo "Located DMS config at: $RESOLVED_PATH" >&2
|
|
||||||
fi
|
|
||||||
QS_CMD="$QS_BIN -p $RESOLVED_PATH"
|
|
||||||
else
|
|
||||||
echo "Error: Could not find DMS config '$DMS_PATH' (shell.qml) in any valid config path" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$COMPOSITOR" in
|
|
||||||
niri)
|
|
||||||
require_command "niri"
|
|
||||||
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
|
|
||||||
TEMP_CONFIG=$(mktemp)
|
|
||||||
# base/default config
|
|
||||||
cat > "$TEMP_CONFIG" << NIRI_EOF
|
|
||||||
hotkey-overlay {
|
|
||||||
skip-at-startup
|
|
||||||
}
|
|
||||||
|
|
||||||
environment {
|
|
||||||
DMS_RUN_GREETER "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
gestures {
|
|
||||||
hot-corners {
|
|
||||||
off
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layout {
|
|
||||||
background-color "#000000"
|
|
||||||
}
|
|
||||||
NIRI_EOF
|
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
|
||||||
else
|
|
||||||
TEMP_CONFIG=$(mktemp)
|
|
||||||
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
|
|
||||||
fi
|
|
||||||
# Append includes if override_files exist
|
|
||||||
OVERRIDE_FILES=(
|
|
||||||
/usr/share/greetd/niri_overrides.kdl # for building into a system image
|
|
||||||
/etc/greetd/niri_overrides.kdl # for local overrides
|
|
||||||
)
|
|
||||||
for override_file in "${OVERRIDE_FILES[@]}"; do
|
|
||||||
if [[ -e "$override_file" ]]; then
|
|
||||||
# TODO: maybe move to optional=true when Niri Next drops, so we can avoid these checks ;-)
|
|
||||||
cat >> "$TEMP_CONFIG" << NIRI_EOF
|
|
||||||
|
|
||||||
include "$override_file"
|
|
||||||
NIRI_EOF
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
# Append spawn command
|
|
||||||
cat >> "$TEMP_CONFIG" << NIRI_EOF
|
|
||||||
|
|
||||||
spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation"
|
|
||||||
NIRI_EOF
|
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
|
||||||
exec_compositor "niri" niri -c "$COMPOSITOR_CONFIG"
|
|
||||||
;;
|
|
||||||
|
|
||||||
hyprland)
|
|
||||||
if ! command -v start-hyprland >/dev/null 2>&1 && ! command -v Hyprland >/dev/null 2>&1; then
|
|
||||||
echo "Error: neither 'start-hyprland' nor 'Hyprland' was found in PATH" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ -n "$COMPOSITOR_CONFIG" ]] && is_hyprland_lua_config "$COMPOSITOR_CONFIG"; then
|
|
||||||
TEMP_CONFIG=$(mktemp --suffix=.lua)
|
|
||||||
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
|
|
||||||
cat >> "$TEMP_CONFIG" << HYPRLAND_LUA_EOF
|
|
||||||
|
|
||||||
hl.on("hyprland.start", function()
|
|
||||||
hl.exec_cmd('sh -c "$QS_CMD; hyprctl dispatch exit"')
|
|
||||||
end)
|
|
||||||
HYPRLAND_LUA_EOF
|
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
|
||||||
elif [[ -z "$COMPOSITOR_CONFIG" ]]; then
|
|
||||||
TEMP_CONFIG=$(mktemp)
|
|
||||||
cat > "$TEMP_CONFIG" << HYPRLAND_EOF
|
|
||||||
env = DMS_RUN_GREETER,1
|
|
||||||
|
|
||||||
misc {
|
|
||||||
disable_hyprland_logo = true
|
|
||||||
}
|
|
||||||
|
|
||||||
exec-once = sh -c "$QS_CMD; hyprctl dispatch exit"
|
|
||||||
HYPRLAND_EOF
|
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
|
||||||
else
|
|
||||||
TEMP_CONFIG=$(mktemp)
|
|
||||||
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
|
|
||||||
cat >> "$TEMP_CONFIG" << HYPRLAND_EOF
|
|
||||||
|
|
||||||
exec-once = sh -c "$QS_CMD; hyprctl dispatch exit"
|
|
||||||
HYPRLAND_EOF
|
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
|
||||||
fi
|
|
||||||
if command -v start-hyprland >/dev/null 2>&1; then
|
|
||||||
exec_compositor "hyprland" start-hyprland -- --config "$COMPOSITOR_CONFIG"
|
|
||||||
else
|
|
||||||
exec_compositor "hyprland" Hyprland -c "$COMPOSITOR_CONFIG"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
|
|
||||||
sway)
|
|
||||||
require_command "sway"
|
|
||||||
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
|
|
||||||
TEMP_CONFIG=$(mktemp)
|
|
||||||
cat > "$TEMP_CONFIG" << SWAY_EOF
|
|
||||||
exec "$QS_CMD; swaymsg exit"
|
|
||||||
SWAY_EOF
|
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
|
||||||
else
|
|
||||||
TEMP_CONFIG=$(mktemp)
|
|
||||||
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
|
|
||||||
cat >> "$TEMP_CONFIG" << SWAY_EOF
|
|
||||||
|
|
||||||
exec "$QS_CMD; swaymsg exit"
|
|
||||||
SWAY_EOF
|
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
|
||||||
fi
|
|
||||||
exec_compositor "sway" sway --unsupported-gpu -c "$COMPOSITOR_CONFIG"
|
|
||||||
;;
|
|
||||||
|
|
||||||
scroll)
|
|
||||||
require_command "scroll"
|
|
||||||
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
|
|
||||||
TEMP_CONFIG=$(mktemp)
|
|
||||||
cat > "$TEMP_CONFIG" << SCROLL_EOF
|
|
||||||
exec "$QS_CMD; scrollmsg exit"
|
|
||||||
SCROLL_EOF
|
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
|
||||||
else
|
|
||||||
TEMP_CONFIG=$(mktemp)
|
|
||||||
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
|
|
||||||
cat >> "$TEMP_CONFIG" << SCROLL_EOF
|
|
||||||
|
|
||||||
exec "$QS_CMD; scrollmsg exit"
|
|
||||||
SCROLL_EOF
|
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
|
||||||
fi
|
|
||||||
exec_compositor "scroll" scroll -c "$COMPOSITOR_CONFIG"
|
|
||||||
;;
|
|
||||||
|
|
||||||
miracle|miracle-wm)
|
|
||||||
require_command "miracle-wm"
|
|
||||||
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
|
|
||||||
TEMP_CONFIG=$(mktemp)
|
|
||||||
cat > "$TEMP_CONFIG" << MIRACLE_EOF
|
|
||||||
exec "$QS_CMD; miraclemsg exit"
|
|
||||||
MIRACLE_EOF
|
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
|
||||||
else
|
|
||||||
TEMP_CONFIG=$(mktemp)
|
|
||||||
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
|
|
||||||
cat >> "$TEMP_CONFIG" << MIRACLE_EOF
|
|
||||||
|
|
||||||
exec "$QS_CMD; miraclemsg exit"
|
|
||||||
MIRACLE_EOF
|
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
|
||||||
fi
|
|
||||||
exec_compositor "miracle" miracle-wm -c "$COMPOSITOR_CONFIG"
|
|
||||||
;;
|
|
||||||
|
|
||||||
labwc)
|
|
||||||
require_command "labwc"
|
|
||||||
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
|
|
||||||
exec_compositor "labwc" labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD"
|
|
||||||
else
|
|
||||||
exec_compositor "labwc" labwc --session "$QS_CMD"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
|
|
||||||
mango|mangowc)
|
|
||||||
require_command "mango"
|
|
||||||
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
|
|
||||||
exec_compositor "mango" mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg dispatch quit"
|
|
||||||
else
|
|
||||||
exec_compositor "mango" mango -s "$QS_CMD && mmsg dispatch quit"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
|
|
||||||
*)
|
|
||||||
echo "Error: Unsupported compositor: $COMPOSITOR" >&2
|
|
||||||
echo "Supported compositors: niri, hyprland, sway, scroll, miracle, mango, labwc" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# Deprecated: greetd expects Hyprland 0.55+ Lua; use `/etc/greetd/dms-hypr.lua` instead.
|
|
||||||
env = DMS_RUN_GREETER,1
|
|
||||||
|
|
||||||
exec = sh -c "qs -p _DMS_PATH_; hyprctl dispatch exit"
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
-- Minimal Hyprland (Lua) session for greetd — replace _DMS_PATH_ with your DMS checkout.
|
|
||||||
-- Copy to `/etc/greetd/dms-hypr.lua` alongside `greet-hyprland.sh`.
|
|
||||||
|
|
||||||
hl.env("DMS_RUN_GREETER", "1")
|
|
||||||
|
|
||||||
hl.on("hyprland.start", function()
|
|
||||||
hl.exec_cmd('sh -c "qs -p _DMS_PATH_; hyprctl dispatch exit"')
|
|
||||||
end)
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
hotkey-overlay {
|
|
||||||
skip-at-startup
|
|
||||||
}
|
|
||||||
|
|
||||||
environment {
|
|
||||||
DMS_RUN_GREETER "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
spawn-at-startup "sh" "-c" "qs -p _DMS_PATH_; niri msg action quit --skip-confirmation"
|
|
||||||
|
|
||||||
gestures {
|
|
||||||
hot-corners {
|
|
||||||
off
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layout {
|
|
||||||
background-color "#000000"
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
export XDG_SESSION_TYPE=wayland
|
|
||||||
export QT_QPA_PLATFORM=wayland
|
|
||||||
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
|
|
||||||
export EGL_PLATFORM=gbm
|
|
||||||
if command -v start-hyprland >/dev/null 2>&1; then
|
|
||||||
exec start-hyprland -- -c /etc/greetd/dms-hypr.lua
|
|
||||||
else
|
|
||||||
exec Hyprland -c /etc/greetd/dms-hypr.lua
|
|
||||||
fi
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
export XDG_SESSION_TYPE=wayland
|
|
||||||
export QT_QPA_PLATFORM=wayland
|
|
||||||
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
|
|
||||||
export EGL_PLATFORM=gbm
|
|
||||||
|
|
||||||
exec niri -c /etc/greetd/dms-niri.kdl
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
id: customButtonKeyboard
|
|
||||||
circular: false
|
|
||||||
property string text: ""
|
|
||||||
width: 40
|
|
||||||
height: 40
|
|
||||||
property bool isShift: false
|
|
||||||
color: Theme.surface
|
|
||||||
|
|
||||||
property bool isIcon: text === "keyboard_hide" || text === "Backspace" || text === "Enter"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: {
|
|
||||||
if (parent.text === "keyboard_hide")
|
|
||||||
return "keyboard_hide";
|
|
||||||
if (parent.text === "Backspace")
|
|
||||||
return "backspace";
|
|
||||||
if (parent.text === "Enter")
|
|
||||||
return "keyboard_return";
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
size: 20
|
|
||||||
color: Theme.surfaceText
|
|
||||||
visible: parent.isIcon
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: contentItem
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: parent.text
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeXLarge
|
|
||||||
font.weight: Font.Normal
|
|
||||||
visible: !parent.isIcon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,369 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
property Item target
|
|
||||||
height: 60 * 5
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
signal dismissed
|
|
||||||
|
|
||||||
property double rowSpacing: 0.01 * width // horizontal spacing between keyboard
|
|
||||||
property double columnSpacing: 0.02 * height // vertical spacing between keyboard
|
|
||||||
property bool shift: false //Boolean for the shift state
|
|
||||||
property bool symbols: false //Boolean for the symbol state
|
|
||||||
property double columns: 10 // Number of column
|
|
||||||
property double rows: 4 // Number of row
|
|
||||||
|
|
||||||
property string strShift: '\u2191'
|
|
||||||
property string strBackspace: "Backspace"
|
|
||||||
property string strEnter: "Enter"
|
|
||||||
property string strClose: "keyboard_hide"
|
|
||||||
|
|
||||||
property var modelKeyboard: {
|
|
||||||
"row_1": [
|
|
||||||
{
|
|
||||||
"text": 'q',
|
|
||||||
"symbol": '1',
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": 'w',
|
|
||||||
"symbol": '2',
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": 'e',
|
|
||||||
"symbol": '3',
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": 'r',
|
|
||||||
"symbol": '4',
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": 't',
|
|
||||||
"symbol": '5',
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": 'y',
|
|
||||||
"symbol": '6',
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": 'u',
|
|
||||||
"symbol": '7',
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": 'i',
|
|
||||||
"symbol": '8',
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": 'o',
|
|
||||||
"symbol": '9',
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": 'p',
|
|
||||||
"symbol": '0',
|
|
||||||
"width": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"row_2": [
|
|
||||||
{
|
|
||||||
"text": 'a',
|
|
||||||
"symbol": '-',
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": 's',
|
|
||||||
"symbol": '/',
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": 'd',
|
|
||||||
"symbol": ':',
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": 'f',
|
|
||||||
"symbol": ';',
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": 'g',
|
|
||||||
"symbol": '(',
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": 'h',
|
|
||||||
"symbol": ')',
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": 'j',
|
|
||||||
"symbol": '€',
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": 'k',
|
|
||||||
"symbol": '&',
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": 'l',
|
|
||||||
"symbol": '@',
|
|
||||||
"width": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"row_3": [
|
|
||||||
{
|
|
||||||
"text": strShift,
|
|
||||||
"symbol": strShift,
|
|
||||||
"width": 1.5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": 'z',
|
|
||||||
"symbol": '.',
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": 'x',
|
|
||||||
"symbol": ',',
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": 'c',
|
|
||||||
"symbol": '?',
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": 'v',
|
|
||||||
"symbol": '!',
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": 'b',
|
|
||||||
"symbol": "'",
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": 'n',
|
|
||||||
"symbol": "%",
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": 'm',
|
|
||||||
"symbol": '"',
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": strBackspace,
|
|
||||||
"symbol": strBackspace,
|
|
||||||
"width": 1.5
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"row_4": [
|
|
||||||
{
|
|
||||||
"text": strClose,
|
|
||||||
"symbol": strClose,
|
|
||||||
"width": 1.5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "123",
|
|
||||||
"symbol": 'ABC',
|
|
||||||
"width": 1.5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": ' ',
|
|
||||||
"symbol": ' ',
|
|
||||||
"width": 4.5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": '.',
|
|
||||||
"symbol": '.',
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": strEnter,
|
|
||||||
"symbol": strEnter,
|
|
||||||
"width": 1.5
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
//Here is the corresponding table between the ascii and the key event
|
|
||||||
property var tableKeyEvent: {
|
|
||||||
"_0": Qt.Key_0,
|
|
||||||
"_1": Qt.Key_1,
|
|
||||||
"_2": Qt.Key_2,
|
|
||||||
"_3": Qt.Key_3,
|
|
||||||
"_4": Qt.Key_4,
|
|
||||||
"_5": Qt.Key_5,
|
|
||||||
"_6": Qt.Key_6,
|
|
||||||
"_7": Qt.Key_7,
|
|
||||||
"_8": Qt.Key_8,
|
|
||||||
"_9": Qt.Key_9,
|
|
||||||
"_a": Qt.Key_A,
|
|
||||||
"_b": Qt.Key_B,
|
|
||||||
"_c": Qt.Key_C,
|
|
||||||
"_d": Qt.Key_D,
|
|
||||||
"_e": Qt.Key_E,
|
|
||||||
"_f": Qt.Key_F,
|
|
||||||
"_g": Qt.Key_G,
|
|
||||||
"_h": Qt.Key_H,
|
|
||||||
"_i": Qt.Key_I,
|
|
||||||
"_j": Qt.Key_J,
|
|
||||||
"_k": Qt.Key_K,
|
|
||||||
"_l": Qt.Key_L,
|
|
||||||
"_m": Qt.Key_M,
|
|
||||||
"_n": Qt.Key_N,
|
|
||||||
"_o": Qt.Key_O,
|
|
||||||
"_p": Qt.Key_P,
|
|
||||||
"_q": Qt.Key_Q,
|
|
||||||
"_r": Qt.Key_R,
|
|
||||||
"_s": Qt.Key_S,
|
|
||||||
"_t": Qt.Key_T,
|
|
||||||
"_u": Qt.Key_U,
|
|
||||||
"_v": Qt.Key_V,
|
|
||||||
"_w": Qt.Key_W,
|
|
||||||
"_x": Qt.Key_X,
|
|
||||||
"_y": Qt.Key_Y,
|
|
||||||
"_z": Qt.Key_Z,
|
|
||||||
"_←": Qt.Key_Backspace,
|
|
||||||
"_return": Qt.Key_Return,
|
|
||||||
"_ ": Qt.Key_Space,
|
|
||||||
"_-": Qt.Key_Minus,
|
|
||||||
"_/": Qt.Key_Slash,
|
|
||||||
"_:": Qt.Key_Colon,
|
|
||||||
"_;": Qt.Key_Semicolon,
|
|
||||||
"_(": Qt.Key_BracketLeft,
|
|
||||||
"_)": Qt.Key_BracketRight,
|
|
||||||
"_€": parseInt("20ac", 16) // I didn't find the appropriate Qt event so I used the hex format
|
|
||||||
,
|
|
||||||
"_&": Qt.Key_Ampersand,
|
|
||||||
"_@": Qt.Key_At,
|
|
||||||
'_"': Qt.Key_QuoteDbl,
|
|
||||||
"_.": Qt.Key_Period,
|
|
||||||
"_,": Qt.Key_Comma,
|
|
||||||
"_?": Qt.Key_Question,
|
|
||||||
"_!": Qt.Key_Exclam,
|
|
||||||
"_'": Qt.Key_Apostrophe,
|
|
||||||
"_%": Qt.Key_Percent,
|
|
||||||
"_*": Qt.Key_Asterisk
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: keyboard_container
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 5
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.topMargin: 5
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.bottomMargin: 5
|
|
||||||
|
|
||||||
//One column which contains 5 rows
|
|
||||||
Column {
|
|
||||||
spacing: columnSpacing
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: row_1
|
|
||||||
spacing: rowSpacing
|
|
||||||
Repeater {
|
|
||||||
model: modelKeyboard["row_1"]
|
|
||||||
delegate: CustomButtonKeyboard {
|
|
||||||
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text
|
|
||||||
width: modelData.width * keyboard_container.width / columns - rowSpacing
|
|
||||||
height: keyboard_container.height / rows - columnSpacing
|
|
||||||
|
|
||||||
onClicked: root.clicked(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row {
|
|
||||||
id: row_2
|
|
||||||
spacing: rowSpacing
|
|
||||||
Repeater {
|
|
||||||
model: modelKeyboard["row_2"]
|
|
||||||
delegate: CustomButtonKeyboard {
|
|
||||||
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text
|
|
||||||
width: modelData.width * keyboard_container.width / columns - rowSpacing
|
|
||||||
height: keyboard_container.height / rows - columnSpacing
|
|
||||||
|
|
||||||
onClicked: root.clicked(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row {
|
|
||||||
id: row_3
|
|
||||||
spacing: rowSpacing
|
|
||||||
Repeater {
|
|
||||||
model: modelKeyboard["row_3"]
|
|
||||||
delegate: CustomButtonKeyboard {
|
|
||||||
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text
|
|
||||||
width: modelData.width * keyboard_container.width / columns - rowSpacing
|
|
||||||
height: keyboard_container.height / rows - columnSpacing
|
|
||||||
isShift: shift && text === strShift
|
|
||||||
|
|
||||||
onClicked: root.clicked(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row {
|
|
||||||
id: row_4
|
|
||||||
spacing: rowSpacing
|
|
||||||
Repeater {
|
|
||||||
model: modelKeyboard["row_4"]
|
|
||||||
delegate: CustomButtonKeyboard {
|
|
||||||
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text
|
|
||||||
width: modelData.width * keyboard_container.width / columns - rowSpacing
|
|
||||||
height: keyboard_container.height / rows - columnSpacing
|
|
||||||
|
|
||||||
onClicked: root.clicked(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
signal clicked(string text)
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
function onClicked(text) {
|
|
||||||
if (!keyboard_controller.target)
|
|
||||||
return;
|
|
||||||
if (text === strShift) {
|
|
||||||
root.shift = !root.shift;
|
|
||||||
} else if (text === '123') {
|
|
||||||
root.symbols = true;
|
|
||||||
} else if (text === 'ABC') {
|
|
||||||
root.symbols = false;
|
|
||||||
} else if (text === strEnter) {
|
|
||||||
if (keyboard_controller.target.accepted) {
|
|
||||||
keyboard_controller.target.accepted();
|
|
||||||
}
|
|
||||||
} else if (text === strClose) {
|
|
||||||
root.dismissed();
|
|
||||||
} else {
|
|
||||||
if (text === strBackspace) {
|
|
||||||
keyboard_controller.target.backspace();
|
|
||||||
} else {
|
|
||||||
var charToInsert = root.symbols ? text : (root.shift ? text.toUpperCase() : text);
|
|
||||||
keyboard_controller.target.insertText(charToInsert);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.shift && text !== strShift)
|
|
||||||
root.shift = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: keyboard_controller
|
|
||||||
readonly property var log: Log.scoped("KeyboardController")
|
|
||||||
|
|
||||||
// reference on the TextInput
|
|
||||||
property Item target
|
|
||||||
//Booléan on the state of the keyboard
|
|
||||||
property bool isKeyboardActive: false
|
|
||||||
|
|
||||||
property var rootObject
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
if (!isKeyboardActive && keyboard === null) {
|
|
||||||
keyboard = keyboardComponent.createObject(keyboard_controller.rootObject);
|
|
||||||
keyboard.target = keyboard_controller.target;
|
|
||||||
keyboard.dismissed.connect(hide);
|
|
||||||
isKeyboardActive = true;
|
|
||||||
} else
|
|
||||||
log.debug("The keyboard is already shown");
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
if (isKeyboardActive && keyboard !== null) {
|
|
||||||
keyboard.destroy();
|
|
||||||
isKeyboardActive = false;
|
|
||||||
} else
|
|
||||||
log.debug("The keyboard is already hidden");
|
|
||||||
}
|
|
||||||
|
|
||||||
// private
|
|
||||||
property Item keyboard: null
|
|
||||||
Component {
|
|
||||||
id: keyboardComponent
|
|
||||||
Keyboard {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,827 +0,0 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool isVisible: false
|
|
||||||
property bool showLogout: true
|
|
||||||
property int selectedIndex: 0
|
|
||||||
property int selectedRow: 0
|
|
||||||
property int selectedCol: 0
|
|
||||||
property var visibleActions: []
|
|
||||||
property int gridColumns: 3
|
|
||||||
property int gridRows: 2
|
|
||||||
property bool useGridLayout: false
|
|
||||||
|
|
||||||
property string holdAction: ""
|
|
||||||
property int holdActionIndex: -1
|
|
||||||
property real holdProgress: 0
|
|
||||||
property bool showHoldHint: false
|
|
||||||
|
|
||||||
property var powerActionConfirmOverride: undefined
|
|
||||||
property var powerActionHoldDurationOverride: undefined
|
|
||||||
property var powerMenuActionsOverride: undefined
|
|
||||||
property var powerMenuDefaultActionOverride: undefined
|
|
||||||
property var powerMenuGridLayoutOverride: undefined
|
|
||||||
property var requiredActions: []
|
|
||||||
|
|
||||||
readonly property bool needsConfirmation: powerActionConfirmOverride !== undefined ? powerActionConfirmOverride : SettingsData.powerActionConfirm
|
|
||||||
readonly property int holdDurationMs: (powerActionHoldDurationOverride !== undefined ? powerActionHoldDurationOverride : SettingsData.powerActionHoldDuration) * 1000
|
|
||||||
|
|
||||||
signal closed
|
|
||||||
|
|
||||||
signal switchUserRequested
|
|
||||||
|
|
||||||
function updateVisibleActions() {
|
|
||||||
const allActions = powerMenuActionsOverride !== undefined ? powerMenuActionsOverride : ((typeof SettingsData !== "undefined" && SettingsData.powerMenuActions) ? SettingsData.powerMenuActions : ["logout", "suspend", "hibernate", "reboot", "poweroff"]);
|
|
||||||
const hibernateSupported = (typeof SessionService !== "undefined" && SessionService.hibernateSupported) || false;
|
|
||||||
let filtered = allActions.filter(action => {
|
|
||||||
if (action === "hibernate" && !hibernateSupported)
|
|
||||||
return false;
|
|
||||||
if (action === "lock")
|
|
||||||
return false;
|
|
||||||
if (action === "restart")
|
|
||||||
return false;
|
|
||||||
if (action === "logout" && !showLogout)
|
|
||||||
return false;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const action of requiredActions) {
|
|
||||||
if (!filtered.includes(action))
|
|
||||||
filtered.push(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
visibleActions = filtered;
|
|
||||||
|
|
||||||
useGridLayout = powerMenuGridLayoutOverride !== undefined ? powerMenuGridLayoutOverride : ((typeof SettingsData !== "undefined" && SettingsData.powerMenuGridLayout !== undefined) ? SettingsData.powerMenuGridLayout : false);
|
|
||||||
if (!useGridLayout)
|
|
||||||
return;
|
|
||||||
const count = visibleActions.length;
|
|
||||||
if (count === 0) {
|
|
||||||
gridColumns = 1;
|
|
||||||
gridRows = 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count <= 3) {
|
|
||||||
gridColumns = 1;
|
|
||||||
gridRows = count;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count === 4) {
|
|
||||||
gridColumns = 2;
|
|
||||||
gridRows = 2;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
gridColumns = 3;
|
|
||||||
gridRows = Math.ceil(count / 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDefaultActionIndex() {
|
|
||||||
const defaultAction = powerMenuDefaultActionOverride !== undefined ? powerMenuDefaultActionOverride : ((typeof SettingsData !== "undefined" && SettingsData.powerMenuDefaultAction) ? SettingsData.powerMenuDefaultAction : "suspend");
|
|
||||||
const index = visibleActions.indexOf(defaultAction);
|
|
||||||
return index >= 0 ? index : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getActionAtIndex(index) {
|
|
||||||
if (index < 0 || index >= visibleActions.length)
|
|
||||||
return "";
|
|
||||||
return visibleActions[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getActionData(action) {
|
|
||||||
switch (action) {
|
|
||||||
case "reboot":
|
|
||||||
return {
|
|
||||||
"icon": "restart_alt",
|
|
||||||
"label": I18n.tr("Reboot"),
|
|
||||||
"key": "R"
|
|
||||||
};
|
|
||||||
case "logout":
|
|
||||||
return {
|
|
||||||
"icon": "logout",
|
|
||||||
"label": I18n.tr("Log Out"),
|
|
||||||
"key": "X"
|
|
||||||
};
|
|
||||||
case "poweroff":
|
|
||||||
return {
|
|
||||||
"icon": "power_settings_new",
|
|
||||||
"label": I18n.tr("Power Off"),
|
|
||||||
"key": "P"
|
|
||||||
};
|
|
||||||
case "suspend":
|
|
||||||
return {
|
|
||||||
"icon": "bedtime",
|
|
||||||
"label": I18n.tr("Suspend"),
|
|
||||||
"key": "S"
|
|
||||||
};
|
|
||||||
case "hibernate":
|
|
||||||
return {
|
|
||||||
"icon": "ac_unit",
|
|
||||||
"label": I18n.tr("Hibernate"),
|
|
||||||
"key": "H"
|
|
||||||
};
|
|
||||||
case "switchuser":
|
|
||||||
return {
|
|
||||||
"icon": "switch_account",
|
|
||||||
"label": I18n.tr("Switch User"),
|
|
||||||
"key": "U"
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
"icon": "help",
|
|
||||||
"label": action,
|
|
||||||
"key": "?"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function actionNeedsConfirm(action) {
|
|
||||||
return action !== "lock" && action !== "restart";
|
|
||||||
}
|
|
||||||
|
|
||||||
function startHold(action, actionIndex) {
|
|
||||||
if (!needsConfirmation || !actionNeedsConfirm(action)) {
|
|
||||||
executeAction(action);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
holdAction = action;
|
|
||||||
holdActionIndex = actionIndex;
|
|
||||||
holdProgress = 0;
|
|
||||||
showHoldHint = false;
|
|
||||||
holdTimer.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelHold() {
|
|
||||||
if (holdAction === "")
|
|
||||||
return;
|
|
||||||
const wasHolding = holdProgress > 0;
|
|
||||||
holdTimer.stop();
|
|
||||||
if (wasHolding && holdProgress < 1) {
|
|
||||||
showHoldHint = true;
|
|
||||||
hintTimer.restart();
|
|
||||||
}
|
|
||||||
holdAction = "";
|
|
||||||
holdActionIndex = -1;
|
|
||||||
holdProgress = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function completeHold() {
|
|
||||||
if (holdProgress < 1) {
|
|
||||||
cancelHold();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const action = holdAction;
|
|
||||||
holdTimer.stop();
|
|
||||||
holdAction = "";
|
|
||||||
holdActionIndex = -1;
|
|
||||||
holdProgress = 0;
|
|
||||||
executeAction(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
function executeAction(action) {
|
|
||||||
if (!action)
|
|
||||||
return;
|
|
||||||
if (action === "switchuser") {
|
|
||||||
hide();
|
|
||||||
switchUserRequested();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeof SessionService === "undefined")
|
|
||||||
return;
|
|
||||||
hide();
|
|
||||||
switch (action) {
|
|
||||||
case "logout":
|
|
||||||
SessionService.logout();
|
|
||||||
break;
|
|
||||||
case "suspend":
|
|
||||||
SessionService.suspend();
|
|
||||||
break;
|
|
||||||
case "hibernate":
|
|
||||||
SessionService.hibernate();
|
|
||||||
break;
|
|
||||||
case "reboot":
|
|
||||||
SessionService.reboot();
|
|
||||||
break;
|
|
||||||
case "poweroff":
|
|
||||||
SessionService.poweroff();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectOption(action, actionIndex) {
|
|
||||||
startHold(action, actionIndex !== undefined ? actionIndex : -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
holdAction = "";
|
|
||||||
holdActionIndex = -1;
|
|
||||||
holdProgress = 0;
|
|
||||||
showHoldHint = false;
|
|
||||||
updateVisibleActions();
|
|
||||||
const defaultIndex = getDefaultActionIndex();
|
|
||||||
if (useGridLayout) {
|
|
||||||
selectedRow = Math.floor(defaultIndex / gridColumns);
|
|
||||||
selectedCol = defaultIndex % gridColumns;
|
|
||||||
selectedIndex = defaultIndex;
|
|
||||||
} else {
|
|
||||||
selectedIndex = defaultIndex;
|
|
||||||
}
|
|
||||||
isVisible = true;
|
|
||||||
Qt.callLater(() => powerMenuFocusScope.forceActiveFocus());
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
cancelHold();
|
|
||||||
isVisible = false;
|
|
||||||
closed();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleListNavigation(event, isPressed) {
|
|
||||||
if (!isPressed) {
|
|
||||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_X || event.key === Qt.Key_S || event.key === Qt.Key_H || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) {
|
|
||||||
cancelHold();
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (event.key) {
|
|
||||||
case Qt.Key_Up:
|
|
||||||
case Qt.Key_Backtab:
|
|
||||||
selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length;
|
|
||||||
event.accepted = true;
|
|
||||||
break;
|
|
||||||
case Qt.Key_Down:
|
|
||||||
case Qt.Key_Tab:
|
|
||||||
selectedIndex = (selectedIndex + 1) % visibleActions.length;
|
|
||||||
event.accepted = true;
|
|
||||||
break;
|
|
||||||
case Qt.Key_Return:
|
|
||||||
case Qt.Key_Enter:
|
|
||||||
startHold(getActionAtIndex(selectedIndex), selectedIndex);
|
|
||||||
event.accepted = true;
|
|
||||||
break;
|
|
||||||
case Qt.Key_N:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
selectedIndex = (selectedIndex + 1) % visibleActions.length;
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Qt.Key_P:
|
|
||||||
if (!(event.modifiers & Qt.ControlModifier)) {
|
|
||||||
if (visibleActions.includes("poweroff")) {
|
|
||||||
const idx = visibleActions.indexOf("poweroff");
|
|
||||||
startHold("poweroff", idx);
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length;
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Qt.Key_J:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
selectedIndex = (selectedIndex + 1) % visibleActions.length;
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Qt.Key_K:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length;
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Qt.Key_R:
|
|
||||||
if (visibleActions.includes("reboot")) {
|
|
||||||
startHold("reboot", visibleActions.indexOf("reboot"));
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Qt.Key_X:
|
|
||||||
if (visibleActions.includes("logout")) {
|
|
||||||
startHold("logout", visibleActions.indexOf("logout"));
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Qt.Key_S:
|
|
||||||
if (visibleActions.includes("suspend")) {
|
|
||||||
startHold("suspend", visibleActions.indexOf("suspend"));
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Qt.Key_H:
|
|
||||||
if (visibleActions.includes("hibernate")) {
|
|
||||||
startHold("hibernate", visibleActions.indexOf("hibernate"));
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleGridNavigation(event, isPressed) {
|
|
||||||
if (!isPressed) {
|
|
||||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_X || event.key === Qt.Key_S || event.key === Qt.Key_H || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) {
|
|
||||||
cancelHold();
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (event.key) {
|
|
||||||
case Qt.Key_Left:
|
|
||||||
selectedCol = (selectedCol - 1 + gridColumns) % gridColumns;
|
|
||||||
selectedIndex = selectedRow * gridColumns + selectedCol;
|
|
||||||
event.accepted = true;
|
|
||||||
break;
|
|
||||||
case Qt.Key_Right:
|
|
||||||
selectedCol = (selectedCol + 1) % gridColumns;
|
|
||||||
selectedIndex = selectedRow * gridColumns + selectedCol;
|
|
||||||
event.accepted = true;
|
|
||||||
break;
|
|
||||||
case Qt.Key_Up:
|
|
||||||
case Qt.Key_Backtab:
|
|
||||||
selectedRow = (selectedRow - 1 + gridRows) % gridRows;
|
|
||||||
selectedIndex = selectedRow * gridColumns + selectedCol;
|
|
||||||
event.accepted = true;
|
|
||||||
break;
|
|
||||||
case Qt.Key_Down:
|
|
||||||
case Qt.Key_Tab:
|
|
||||||
selectedRow = (selectedRow + 1) % gridRows;
|
|
||||||
selectedIndex = selectedRow * gridColumns + selectedCol;
|
|
||||||
event.accepted = true;
|
|
||||||
break;
|
|
||||||
case Qt.Key_Return:
|
|
||||||
case Qt.Key_Enter:
|
|
||||||
startHold(getActionAtIndex(selectedIndex), selectedIndex);
|
|
||||||
event.accepted = true;
|
|
||||||
break;
|
|
||||||
case Qt.Key_N:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
selectedCol = (selectedCol + 1) % gridColumns;
|
|
||||||
selectedIndex = selectedRow * gridColumns + selectedCol;
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Qt.Key_P:
|
|
||||||
if (!(event.modifiers & Qt.ControlModifier)) {
|
|
||||||
if (visibleActions.includes("poweroff")) {
|
|
||||||
const idx = visibleActions.indexOf("poweroff");
|
|
||||||
startHold("poweroff", idx);
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selectedCol = (selectedCol - 1 + gridColumns) % gridColumns;
|
|
||||||
selectedIndex = selectedRow * gridColumns + selectedCol;
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Qt.Key_J:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
selectedRow = (selectedRow + 1) % gridRows;
|
|
||||||
selectedIndex = selectedRow * gridColumns + selectedCol;
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Qt.Key_K:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
selectedRow = (selectedRow - 1 + gridRows) % gridRows;
|
|
||||||
selectedIndex = selectedRow * gridColumns + selectedCol;
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Qt.Key_R:
|
|
||||||
if (visibleActions.includes("reboot")) {
|
|
||||||
startHold("reboot", visibleActions.indexOf("reboot"));
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Qt.Key_X:
|
|
||||||
if (visibleActions.includes("logout")) {
|
|
||||||
startHold("logout", visibleActions.indexOf("logout"));
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Qt.Key_S:
|
|
||||||
if (visibleActions.includes("suspend")) {
|
|
||||||
startHold("suspend", visibleActions.indexOf("suspend"));
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Qt.Key_H:
|
|
||||||
if (visibleActions.includes("hibernate")) {
|
|
||||||
startHold("hibernate", visibleActions.indexOf("hibernate"));
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
color: Qt.rgba(0, 0, 0, 0.5)
|
|
||||||
visible: isVisible
|
|
||||||
z: 1000
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: root.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: holdTimer
|
|
||||||
interval: 16
|
|
||||||
repeat: true
|
|
||||||
onTriggered: {
|
|
||||||
root.holdProgress = Math.min(1, root.holdProgress + (interval / root.holdDurationMs));
|
|
||||||
if (root.holdProgress >= 1) {
|
|
||||||
stop();
|
|
||||||
root.completeHold();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: hintTimer
|
|
||||||
interval: 2000
|
|
||||||
onTriggered: root.showHoldHint = false
|
|
||||||
}
|
|
||||||
|
|
||||||
FocusScope {
|
|
||||||
id: powerMenuFocusScope
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: root.isVisible
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible)
|
|
||||||
Qt.callLater(() => forceActiveFocus());
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onEscapePressed: root.hide()
|
|
||||||
Keys.onPressed: event => {
|
|
||||||
if (event.isAutoRepeat) {
|
|
||||||
event.accepted = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (useGridLayout) {
|
|
||||||
handleGridNavigation(event, true);
|
|
||||||
} else {
|
|
||||||
handleListNavigation(event, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Keys.onReleased: event => {
|
|
||||||
if (event.isAutoRepeat) {
|
|
||||||
event.accepted = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (useGridLayout) {
|
|
||||||
handleGridNavigation(event, false);
|
|
||||||
} else {
|
|
||||||
handleListNavigation(event, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: useGridLayout ? Math.min(550, gridColumns * 180 + Theme.spacingS * (gridColumns - 1) + Theme.spacingL * 2) : 320
|
|
||||||
height: contentItem.implicitHeight + Theme.spacingL * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: contentItem
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
implicitHeight: headerRow.height + Theme.spacingM + (useGridLayout ? buttonGrid.implicitHeight : buttonColumn.implicitHeight) + (root.needsConfirmation ? hintRow.height + Theme.spacingM : 0)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: headerRow
|
|
||||||
width: parent.width
|
|
||||||
height: 30
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Power Options")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - 150
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
onClicked: root.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Grid {
|
|
||||||
id: buttonGrid
|
|
||||||
visible: useGridLayout
|
|
||||||
anchors.top: headerRow.bottom
|
|
||||||
anchors.topMargin: Theme.spacingM
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
columns: root.gridColumns
|
|
||||||
columnSpacing: Theme.spacingS
|
|
||||||
rowSpacing: Theme.spacingS
|
|
||||||
width: parent.width
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: root.visibleActions
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: gridButtonRect
|
|
||||||
required property int index
|
|
||||||
required property string modelData
|
|
||||||
|
|
||||||
readonly property var actionData: root.getActionData(modelData)
|
|
||||||
readonly property bool isSelected: root.selectedIndex === index
|
|
||||||
readonly property bool showWarning: modelData === "reboot" || modelData === "poweroff"
|
|
||||||
readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0
|
|
||||||
|
|
||||||
width: (contentItem.width - Theme.spacingS * (root.gridColumns - 1)) / root.gridColumns
|
|
||||||
height: 100
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (isSelected)
|
|
||||||
return Theme.primaryHover;
|
|
||||||
if (mouseArea.containsMouse)
|
|
||||||
return Theme.primaryHoverLight;
|
|
||||||
return Theme.surfaceHover;
|
|
||||||
}
|
|
||||||
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
|
||||||
border.width: isSelected ? 2 : 0
|
|
||||||
|
|
||||||
ClippingRectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: parent.radius
|
|
||||||
color: "transparent"
|
|
||||||
visible: gridButtonRect.isHolding
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
width: parent.width * root.holdProgress
|
|
||||||
color: {
|
|
||||||
if (gridButtonRect.modelData === "poweroff")
|
|
||||||
return Theme.errorSelected;
|
|
||||||
if (gridButtonRect.modelData === "reboot")
|
|
||||||
return Theme.withAlpha(Theme.warning, 0.3);
|
|
||||||
return Theme.primarySelected;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: gridButtonRect.actionData.icon
|
|
||||||
size: Theme.iconSize + 8
|
|
||||||
color: {
|
|
||||||
if (gridButtonRect.showWarning && (mouseArea.containsMouse || gridButtonRect.isHolding)) {
|
|
||||||
return gridButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning;
|
|
||||||
}
|
|
||||||
return Theme.surfaceText;
|
|
||||||
}
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: gridButtonRect.actionData.label
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: {
|
|
||||||
if (gridButtonRect.showWarning && (mouseArea.containsMouse || gridButtonRect.isHolding)) {
|
|
||||||
return gridButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning;
|
|
||||||
}
|
|
||||||
return Theme.surfaceText;
|
|
||||||
}
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 20
|
|
||||||
height: 16
|
|
||||||
radius: 4
|
|
||||||
color: Theme.onSurface_12
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: gridButtonRect.actionData.key
|
|
||||||
font.pixelSize: Theme.fontSizeSmall - 1
|
|
||||||
color: Theme.surfaceTextSecondary
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
root.selectedRow = Math.floor(index / root.gridColumns);
|
|
||||||
root.selectedCol = index % root.gridColumns;
|
|
||||||
root.selectedIndex = index;
|
|
||||||
root.startHold(modelData, index);
|
|
||||||
}
|
|
||||||
onReleased: root.cancelHold()
|
|
||||||
onCanceled: root.cancelHold()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: buttonColumn
|
|
||||||
visible: !useGridLayout
|
|
||||||
anchors.top: headerRow.bottom
|
|
||||||
anchors.topMargin: Theme.spacingM
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: root.visibleActions
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: listButtonRect
|
|
||||||
required property int index
|
|
||||||
required property string modelData
|
|
||||||
|
|
||||||
readonly property var actionData: root.getActionData(modelData)
|
|
||||||
readonly property bool isSelected: root.selectedIndex === index
|
|
||||||
readonly property bool showWarning: modelData === "reboot" || modelData === "poweroff"
|
|
||||||
readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (isSelected)
|
|
||||||
return Theme.primaryHover;
|
|
||||||
if (listMouseArea.containsMouse)
|
|
||||||
return Theme.primaryHoverLight;
|
|
||||||
return Theme.surfaceHover;
|
|
||||||
}
|
|
||||||
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
|
||||||
border.width: isSelected ? 2 : 0
|
|
||||||
|
|
||||||
ClippingRectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: parent.radius
|
|
||||||
color: "transparent"
|
|
||||||
visible: listButtonRect.isHolding
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
width: parent.width * root.holdProgress
|
|
||||||
color: {
|
|
||||||
if (listButtonRect.modelData === "poweroff")
|
|
||||||
return Theme.errorSelected;
|
|
||||||
if (listButtonRect.modelData === "reboot")
|
|
||||||
return Theme.withAlpha(Theme.warning, 0.3);
|
|
||||||
return Theme.primarySelected;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: listButtonRect.actionData.icon
|
|
||||||
size: Theme.iconSize + 4
|
|
||||||
color: {
|
|
||||||
if (listButtonRect.showWarning && (listMouseArea.containsMouse || listButtonRect.isHolding)) {
|
|
||||||
return listButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning;
|
|
||||||
}
|
|
||||||
return Theme.surfaceText;
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: listButtonRect.actionData.label
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: {
|
|
||||||
if (listButtonRect.showWarning && (listMouseArea.containsMouse || listButtonRect.isHolding)) {
|
|
||||||
return listButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning;
|
|
||||||
}
|
|
||||||
return Theme.surfaceText;
|
|
||||||
}
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 28
|
|
||||||
height: 20
|
|
||||||
radius: 4
|
|
||||||
color: Theme.onSurface_12
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: listButtonRect.actionData.key
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceTextSecondary
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: listMouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: {
|
|
||||||
root.selectedIndex = index;
|
|
||||||
root.startHold(modelData, index);
|
|
||||||
}
|
|
||||||
onReleased: root.cancelHold()
|
|
||||||
onCanceled: root.cancelHold()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: hintRow
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.bottomMargin: Theme.spacingS
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
visible: root.needsConfirmation
|
|
||||||
opacity: root.showHoldHint ? 1 : 0.5
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 150
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: root.showHoldHint ? "warning" : "touch_app"
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: root.showHoldHint ? Theme.warning : Theme.surfaceTextSecondary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
readonly property real totalMs: root.holdDurationMs
|
|
||||||
readonly property int remainingMs: Math.ceil(totalMs * (1 - root.holdProgress))
|
|
||||||
readonly property real durationSec: root.holdDurationMs / 1000
|
|
||||||
text: {
|
|
||||||
if (root.showHoldHint)
|
|
||||||
return I18n.tr("Hold longer to confirm");
|
|
||||||
if (root.holdProgress > 0) {
|
|
||||||
if (totalMs < 1000)
|
|
||||||
return I18n.tr("Hold to confirm (%1 ms)").arg(remainingMs);
|
|
||||||
return I18n.tr("Hold to confirm (%1s)").arg(Math.ceil(remainingMs / 1000));
|
|
||||||
}
|
|
||||||
if (totalMs < 1000)
|
|
||||||
return I18n.tr("Hold to confirm (%1 ms)").arg(totalMs);
|
|
||||||
return I18n.tr("Hold to confirm (%1s)").arg(durationSec);
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: root.showHoldHint ? Theme.warning : Theme.surfaceTextSecondary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: updateVisibleActions()
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,8 @@ import qs.Common
|
|||||||
import qs.Modals
|
import qs.Modals
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
import "../../Common/LayoutCodes.js" as LayoutCodes
|
import qs.DankCommon.Session
|
||||||
|
import "../../DankCommon/Common/LayoutCodes.js" as LayoutCodes
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|||||||
@@ -82,27 +82,44 @@ Item {
|
|||||||
property bool greeterBinaryExists: false
|
property bool greeterBinaryExists: false
|
||||||
property bool greeterEnabled: false
|
property bool greeterEnabled: false
|
||||||
readonly property bool greeterInstalled: greeterBinaryExists || greeterEnabled
|
readonly property bool greeterInstalled: greeterBinaryExists || greeterEnabled
|
||||||
|
readonly property string greeterAction: {
|
||||||
|
if (!greeterInstalled)
|
||||||
|
return "install";
|
||||||
|
if (!greeterEnabled)
|
||||||
|
return "activate";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
readonly property bool greeterActionAvailable: greeterAction !== ""
|
||||||
|
|
||||||
readonly property string greeterActionLabel: {
|
readonly property string greeterActionLabel: {
|
||||||
if (!root.greeterInstalled)
|
switch (greeterAction) {
|
||||||
|
case "install":
|
||||||
return I18n.tr("Install");
|
return I18n.tr("Install");
|
||||||
if (!root.greeterEnabled)
|
case "activate":
|
||||||
return I18n.tr("Activate");
|
return I18n.tr("Activate");
|
||||||
return I18n.tr("Uninstall");
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
readonly property string greeterActionIcon: {
|
readonly property string greeterActionIcon: {
|
||||||
if (!root.greeterInstalled)
|
switch (greeterAction) {
|
||||||
|
case "install":
|
||||||
return "download";
|
return "download";
|
||||||
if (!root.greeterEnabled)
|
case "activate":
|
||||||
return "login";
|
return "login";
|
||||||
return "delete";
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
readonly property var greeterActionCommand: {
|
readonly property var greeterActionCommand: {
|
||||||
if (!root.greeterInstalled)
|
switch (greeterAction) {
|
||||||
|
case "install":
|
||||||
return ["dms", "greeter", "install", "--terminal"];
|
return ["dms", "greeter", "install", "--terminal"];
|
||||||
if (!root.greeterEnabled)
|
case "activate":
|
||||||
return ["dms", "greeter", "enable", "--terminal"];
|
return ["dms", "greeter", "enable", "--terminal"];
|
||||||
return ["dms", "greeter", "uninstall", "--terminal", "--yes"];
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
property string greeterPendingAction: ""
|
property string greeterPendingAction: ""
|
||||||
|
|
||||||
@@ -120,26 +137,28 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function runGreeterInstallAction() {
|
function runGreeterInstallAction() {
|
||||||
root.greeterPendingAction = !root.greeterInstalled ? "install" : !root.greeterEnabled ? "activate" : "uninstall";
|
root.greeterPendingAction = root.greeterAction;
|
||||||
greeterStatusText = I18n.tr("Opening terminal: ") + root.greeterActionLabel + "...";
|
greeterStatusText = I18n.tr("Opening terminal: ") + root.greeterActionLabel + "...";
|
||||||
greeterInstallActionRunning = true;
|
greeterInstallActionRunning = true;
|
||||||
greeterInstallActionProcess.running = true;
|
greeterInstallActionProcess.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function promptGreeterActionConfirm() {
|
function promptGreeterActionConfirm() {
|
||||||
|
if (!root.greeterActionAvailable)
|
||||||
|
return;
|
||||||
|
|
||||||
var title, message, confirmText;
|
var title, message, confirmText;
|
||||||
if (!root.greeterInstalled) {
|
switch (root.greeterAction) {
|
||||||
|
case "install":
|
||||||
title = I18n.tr("Install Greeter", "greeter action confirmation");
|
title = I18n.tr("Install Greeter", "greeter action confirmation");
|
||||||
message = I18n.tr("Install the DMS greeter? A terminal will open for sudo authentication.");
|
message = I18n.tr("Install the DMS greeter? A terminal will open for sudo authentication.");
|
||||||
confirmText = I18n.tr("Install");
|
confirmText = I18n.tr("Install");
|
||||||
} else if (!root.greeterEnabled) {
|
break;
|
||||||
|
case "activate":
|
||||||
title = I18n.tr("Activate Greeter", "greeter action confirmation");
|
title = I18n.tr("Activate Greeter", "greeter action confirmation");
|
||||||
message = I18n.tr("Activate the DMS greeter? A terminal will open for sudo authentication. Run Sync after activation to apply your settings.");
|
message = I18n.tr("Activate the DMS greeter? A terminal will open for sudo authentication. Run Sync after activation to apply your settings.");
|
||||||
confirmText = I18n.tr("Activate");
|
confirmText = I18n.tr("Activate");
|
||||||
} else {
|
break;
|
||||||
title = I18n.tr("Uninstall Greeter", "greeter action confirmation");
|
|
||||||
message = I18n.tr("Uninstall the DMS greeter? This will remove configuration and restore your previous display manager. A terminal will open for sudo authentication.");
|
|
||||||
confirmText = I18n.tr("Uninstall");
|
|
||||||
}
|
}
|
||||||
greeterActionConfirm.showWithOptions({
|
greeterActionConfirm.showWithOptions({
|
||||||
"title": title,
|
"title": title,
|
||||||
@@ -247,7 +266,17 @@ Item {
|
|||||||
root.greeterSyncRunning = false;
|
root.greeterSyncRunning = false;
|
||||||
const out = (root.greeterSyncStdout || "").trim();
|
const out = (root.greeterSyncStdout || "").trim();
|
||||||
const err = (root.greeterSyncStderr || "").trim();
|
const err = (root.greeterSyncStderr || "").trim();
|
||||||
if (exitCode === 0) {
|
root.checkGreeterInstallState();
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
var failure = I18n.tr("Sync failed in background mode. Trying terminal mode so you can authenticate interactively.") + " (exit " + exitCode + ")";
|
||||||
|
if (out !== "")
|
||||||
|
failure = failure + "\n\n" + out;
|
||||||
|
if (err !== "")
|
||||||
|
failure = failure + "\n\nstderr:\n" + err;
|
||||||
|
root.greeterStatusText = failure;
|
||||||
|
root.launchGreeterSyncTerminalFallback(false, "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
var success = I18n.tr("Sync completed successfully.");
|
var success = I18n.tr("Sync completed successfully.");
|
||||||
if (out !== "")
|
if (out !== "")
|
||||||
success = success + "\n\n" + out;
|
success = success + "\n\n" + out;
|
||||||
@@ -256,16 +285,6 @@ Item {
|
|||||||
root.greeterStatusText = success;
|
root.greeterStatusText = success;
|
||||||
SettingsData.clearGreeterSyncPending();
|
SettingsData.clearGreeterSyncPending();
|
||||||
ToastService.showInfo(I18n.tr("Greeter sync complete"));
|
ToastService.showInfo(I18n.tr("Greeter sync complete"));
|
||||||
} else {
|
|
||||||
var failure = I18n.tr("Sync failed in background mode. Trying terminal mode so you can authenticate interactively.") + " (exit " + exitCode + ")";
|
|
||||||
if (out !== "")
|
|
||||||
failure = failure + "\n\n" + out;
|
|
||||||
if (err !== "")
|
|
||||||
failure = failure + "\n\nstderr:\n" + err;
|
|
||||||
root.greeterStatusText = failure;
|
|
||||||
root.launchGreeterSyncTerminalFallback(false, "");
|
|
||||||
}
|
|
||||||
root.checkGreeterInstallState();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,17 +346,19 @@ Item {
|
|||||||
root.greeterInstallActionRunning = false;
|
root.greeterInstallActionRunning = false;
|
||||||
const pending = root.greeterPendingAction;
|
const pending = root.greeterPendingAction;
|
||||||
root.greeterPendingAction = "";
|
root.greeterPendingAction = "";
|
||||||
if (exitCode === 0) {
|
|
||||||
if (pending === "install")
|
|
||||||
root.greeterStatusText = I18n.tr("Install complete. Greeter has been installed.");
|
|
||||||
else if (pending === "activate")
|
|
||||||
root.greeterStatusText = I18n.tr("Greeter activated. greetd is now enabled.");
|
|
||||||
else
|
|
||||||
root.greeterStatusText = I18n.tr("Uninstall complete. Greeter has been removed.");
|
|
||||||
} else {
|
|
||||||
root.greeterStatusText = I18n.tr("Action failed or terminal was closed.") + " (exit " + exitCode + ")";
|
|
||||||
}
|
|
||||||
root.checkGreeterInstallState();
|
root.checkGreeterInstallState();
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
root.greeterStatusText = I18n.tr("Action failed or terminal was closed.") + " (exit " + exitCode + ")";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (pending) {
|
||||||
|
case "install":
|
||||||
|
root.greeterStatusText = I18n.tr("Install complete. Greeter has been installed.");
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
root.greeterStatusText = I18n.tr("Greeter activated. greetd is now enabled.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,7 +444,13 @@ Item {
|
|||||||
id: statusTextArea
|
id: statusTextArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Theme.spacingM
|
anchors.margins: Theme.spacingM
|
||||||
text: root.greeterStatusRunning ? I18n.tr("Checking...", "greeter status loading") : (root.greeterStatusText || I18n.tr("Click Refresh to check status.", "greeter status placeholder"))
|
text: {
|
||||||
|
if (root.greeterStatusRunning)
|
||||||
|
return I18n.tr("Checking...", "greeter status loading");
|
||||||
|
if (root.greeterStatusText !== "")
|
||||||
|
return root.greeterStatusText;
|
||||||
|
return I18n.tr("Click Refresh to check status.", "greeter status placeholder");
|
||||||
|
}
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
font.family: "monospace"
|
font.family: "monospace"
|
||||||
color: root.greeterStatusRunning ? Theme.surfaceVariantText : Theme.surfaceText
|
color: root.greeterStatusRunning ? Theme.surfaceVariantText : Theme.surfaceText
|
||||||
@@ -442,6 +469,7 @@ Item {
|
|||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
DankButton {
|
DankButton {
|
||||||
|
visible: root.greeterActionAvailable
|
||||||
text: root.greeterActionLabel
|
text: root.greeterActionLabel
|
||||||
iconName: root.greeterActionIcon
|
iconName: root.greeterActionIcon
|
||||||
horizontalPadding: Theme.spacingL
|
horizontalPadding: Theme.spacingL
|
||||||
@@ -556,9 +584,13 @@ Item {
|
|||||||
description: I18n.tr("Format the date on the login screen")
|
description: I18n.tr("Format the date on the login screen")
|
||||||
options: root._lockDateFormatPresets.map(p => p.label)
|
options: root._lockDateFormatPresets.map(p => p.label)
|
||||||
currentValue: {
|
currentValue: {
|
||||||
var current = (SettingsData.greeterLockDateFormat !== undefined && SettingsData.greeterLockDateFormat !== "") ? SettingsData.greeterLockDateFormat : SettingsData.lockDateFormat || "";
|
var current = SettingsData.greeterLockDateFormat || SettingsData.lockDateFormat || "";
|
||||||
var match = root._lockDateFormatPresets.find(p => p.format === current);
|
var match = root._lockDateFormatPresets.find(p => p.format === current);
|
||||||
return match ? match.label : (current ? I18n.tr("Custom") + ": " + current : root._lockDateFormatPresets[0].label);
|
if (match)
|
||||||
|
return match.label;
|
||||||
|
if (current)
|
||||||
|
return I18n.tr("Custom") + ": " + current;
|
||||||
|
return root._lockDateFormatPresets[0].label;
|
||||||
}
|
}
|
||||||
onValueChanged: value => {
|
onValueChanged: value => {
|
||||||
var preset = root._lockDateFormatPresets.find(p => p.label === value);
|
var preset = root._lockDateFormatPresets.find(p => p.label === value);
|
||||||
|
|||||||
@@ -13,12 +13,7 @@ Variants {
|
|||||||
// An entry present in PanelWindow.onCompleted means we're recreating
|
// An entry present in PanelWindow.onCompleted means we're recreating
|
||||||
// after a wl_output rebind, not at initial startup.
|
// after a wl_output rebind, not at initial startup.
|
||||||
property var _seenScreens: ({})
|
property var _seenScreens: ({})
|
||||||
model: {
|
model: SettingsData.getFilteredScreens("wallpaper")
|
||||||
if (SessionData.isGreeterMode) {
|
|
||||||
return Quickshell.screens;
|
|
||||||
}
|
|
||||||
return SettingsData.getFilteredScreens("wallpaper");
|
|
||||||
}
|
|
||||||
|
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: wallpaperWindow
|
id: wallpaperWindow
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ make lint-qml # Run from repo root; requires quickshell/.qmlls.ini (generated b
|
|||||||
**System Controls**
|
**System Controls**
|
||||||
- `Modules/ControlCenter/` - WiFi, Bluetooth, audio, display settings
|
- `Modules/ControlCenter/` - WiFi, Bluetooth, audio, display settings
|
||||||
- `Modules/Notifications/` - Notification center with popups
|
- `Modules/Notifications/` - Notification center with popups
|
||||||
- `Modules/Greetd/` - Login greeter interface
|
|
||||||
|
|
||||||
**Overlays**
|
**Overlays**
|
||||||
- `Modules/Spotlight/` - Application and file launcher
|
- `Modules/Spotlight/` - Application and file launcher
|
||||||
|
|||||||
@@ -1,163 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property var log: Log.scoped("GreeterUsersService")
|
|
||||||
|
|
||||||
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
|
|
||||||
readonly property string usersCacheDir: greetCfgDir + "/users"
|
|
||||||
|
|
||||||
property var users: []
|
|
||||||
property var usernames: []
|
|
||||||
property var profileImageMap: ({})
|
|
||||||
property bool loaded: false
|
|
||||||
property bool refreshing: false
|
|
||||||
|
|
||||||
Component.onCompleted: refresh()
|
|
||||||
|
|
||||||
function refresh() {
|
|
||||||
if (refreshing)
|
|
||||||
return;
|
|
||||||
refreshing = true;
|
|
||||||
_loadUsers();
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayName(username) {
|
|
||||||
const u = _findUser(username);
|
|
||||||
if (!u)
|
|
||||||
return username || "";
|
|
||||||
const gecos = (u.gecos || "").trim();
|
|
||||||
return gecos.length > 0 ? gecos : username;
|
|
||||||
}
|
|
||||||
|
|
||||||
function optionLabel(username) {
|
|
||||||
const label = displayName(username);
|
|
||||||
return label !== username ? label : username;
|
|
||||||
}
|
|
||||||
|
|
||||||
function usernameFromOptionLabel(label) {
|
|
||||||
for (let i = 0; i < users.length; i++) {
|
|
||||||
if (root.optionLabel(users[i].username) === label)
|
|
||||||
return users[i].username;
|
|
||||||
}
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasSyncedTheme(username) {
|
|
||||||
if (!username)
|
|
||||||
return false;
|
|
||||||
return syncedThemePaths[username] === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
property var syncedThemePaths: ({})
|
|
||||||
|
|
||||||
function userCacheDir(username) {
|
|
||||||
if (!username)
|
|
||||||
return "";
|
|
||||||
return usersCacheDir + "/" + username;
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncedSettingsPath(username) {
|
|
||||||
const dir = userCacheDir(username);
|
|
||||||
return dir ? dir + "/settings.json" : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function _findUser(name) {
|
|
||||||
for (let i = 0; i < users.length; i++) {
|
|
||||||
if (users[i].username === name)
|
|
||||||
return users[i];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _loadUsers() {
|
|
||||||
Proc.runCommand("greeterUsersService-loadUsers", ["sh", "-c", "getent passwd | awk -F: '$3>=1000 && $3<60000 && $1!=\"nobody\" && $7!~/(nologin|false)$/ && $6!=\"/var/empty\" {print $1\":\"$3\":\"$5\":\"$6\":\"$7}'"], (output, exitCode) => {
|
|
||||||
const lines = (output || "").trim().split("\n").filter(l => l.length > 0);
|
|
||||||
const list = [];
|
|
||||||
const names = [];
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const parts = lines[i].split(":");
|
|
||||||
if (parts.length < 5)
|
|
||||||
continue;
|
|
||||||
const username = parts[0];
|
|
||||||
list.push({
|
|
||||||
username,
|
|
||||||
uid: parseInt(parts[1], 10),
|
|
||||||
gecos: (parts[2] || "").split(",")[0],
|
|
||||||
home: parts[3] || "",
|
|
||||||
shell: parts[4] || ""
|
|
||||||
});
|
|
||||||
names.push(username);
|
|
||||||
}
|
|
||||||
list.sort((a, b) => a.username.localeCompare(b.username));
|
|
||||||
names.sort((a, b) => a.localeCompare(b));
|
|
||||||
root.users = list;
|
|
||||||
root.usernames = names;
|
|
||||||
root.loaded = true;
|
|
||||||
root.refreshing = false;
|
|
||||||
_refreshSyncedThemeFlags();
|
|
||||||
_loadProfileIcons();
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _refreshSyncedThemeFlags() {
|
|
||||||
if (usernames.length === 0) {
|
|
||||||
syncedThemePaths = ({});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const checks = usernames.map(u => `[ -f "${syncedSettingsPath(u)}" ] && echo "${u}:1" || echo "${u}:0"`).join("; ");
|
|
||||||
Proc.runCommand("greeterUsersService-syncedThemes", ["sh", "-c", checks], (output, exitCode) => {
|
|
||||||
const map = {};
|
|
||||||
const lines = (output || "").trim().split("\n").filter(l => l.length > 0);
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const parts = lines[i].split(":");
|
|
||||||
if (parts.length >= 2)
|
|
||||||
map[parts[0]] = parts[1] === "1";
|
|
||||||
}
|
|
||||||
root.syncedThemePaths = map;
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function profileImagePath(username) {
|
|
||||||
if (!username)
|
|
||||||
return "";
|
|
||||||
return profileImageMap[username] || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function _loadProfileIcons() {
|
|
||||||
if (users.length === 0) {
|
|
||||||
profileImageMap = ({});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const script = users.map(u => {
|
|
||||||
const safeUser = u.username.replace(/'/g, "'\\''");
|
|
||||||
const safeHome = (u.home || "").replace(/'/g, "'\\''");
|
|
||||||
const cacheDir = usersCacheDir + "/" + u.username;
|
|
||||||
return `( icon=""; for f in "${cacheDir}/profile.jpg" "${cacheDir}/profile.jpeg" "${cacheDir}/profile.png" "${cacheDir}/profile.webp" "/var/lib/AccountsService/icons/${safeUser}" "${safeHome}/.face" "${safeHome}/.face.icon"; do if [ -f "$f" ] && [ -r "$f" ]; then icon="$f"; break; fi; done; echo "${u.username}:$icon" )`;
|
|
||||||
}).join("; ");
|
|
||||||
Proc.runCommand("greeterUsersService-profileIcons", ["sh", "-c", script], (output, exitCode) => {
|
|
||||||
const map = {};
|
|
||||||
const lines = (output || "").trim().split("\n").filter(l => l.length > 0);
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const idx = lines[i].indexOf(":");
|
|
||||||
if (idx <= 0)
|
|
||||||
continue;
|
|
||||||
const user = lines[i].substring(0, idx);
|
|
||||||
const icon = lines[i].substring(idx + 1).trim();
|
|
||||||
map[user] = icon && icon.length > 0 ? icon : "";
|
|
||||||
}
|
|
||||||
for (let j = 0; j < users.length; j++) {
|
|
||||||
const u = users[j].username;
|
|
||||||
if (!(u in map))
|
|
||||||
map[u] = "";
|
|
||||||
}
|
|
||||||
root.profileImageMap = map;
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -53,10 +53,6 @@ Singleton {
|
|||||||
profileImage = "";
|
profileImage = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true") {
|
|
||||||
profileImage = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!freedeskAvailable) {
|
if (!freedeskAvailable) {
|
||||||
profileImage = "";
|
profileImage = "";
|
||||||
@@ -299,52 +295,6 @@ Singleton {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
property string pendingGreeterProfileUser: ""
|
|
||||||
|
|
||||||
function getGreeterUserProfileImage(username) {
|
|
||||||
if (!username) {
|
|
||||||
profileImage = "";
|
|
||||||
pendingGreeterProfileUser = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeof GreeterUsersService !== "undefined") {
|
|
||||||
const cachedPath = GreeterUsersService.profileImagePath(username);
|
|
||||||
if (cachedPath) {
|
|
||||||
profileImage = cachedPath;
|
|
||||||
pendingGreeterProfileUser = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pendingGreeterProfileUser = username;
|
|
||||||
userProfileCheckProcess.command = ["bash", "-c", `uid=$(id -u ${username} 2>/dev/null) && [ -n "$uid" ] && dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$uid org.freedesktop.DBus.Properties.Get string:org.freedesktop.Accounts.User string:IconFile 2>/dev/null | grep -oP 'string "\\K[^"]+' || echo ""`];
|
|
||||||
userProfileCheckProcess.running = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: userProfileCheckProcess
|
|
||||||
command: []
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
const trimmed = text.trim();
|
|
||||||
if (trimmed && trimmed !== "" && !trimmed.includes("Error") && trimmed !== "/var/lib/AccountsService/icons/") {
|
|
||||||
root.profileImage = trimmed;
|
|
||||||
} else {
|
|
||||||
root.profileImage = "";
|
|
||||||
}
|
|
||||||
root.pendingGreeterProfileUser = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode !== 0 && root.pendingGreeterProfileUser !== "") {
|
|
||||||
root.profileImage = "";
|
|
||||||
root.pendingGreeterProfileUser = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: colorSchemeDetector
|
id: colorSchemeDetector
|
||||||
command: ["bash", "-c", "command -v gsettings || command -v dconf"]
|
command: ["bash", "-c", "command -v gsettings || command -v dconf"]
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user