From a69acf171affd53aa01aa9ed7bd437e5e9ed31f9 Mon Sep 17 00:00:00 2001 From: bbedward Date: Sun, 19 Jul 2026 14:18:26 -0400 Subject: [PATCH] greeter: migrated dms-greeter to github/AvengeMedia/dank-greeter --- .github/workflows/nix-pr-check.yml | 2 - .github/workflows/run-copr.yml | 12 +- .github/workflows/run-obs.yml | 66 +- .github/workflows/run-ppa.yml | 3 - .github/workflows/run-xbps.yml | 36 +- README.md | 13 +- core/README.md | 6 +- core/cmd/dms/commands_auth.go | 31 +- core/cmd/dms/commands_greeter.go | 2138 +------------ core/cmd/dms/commands_greeter_test.go | 150 - core/cmd/dms/commands_setup.go | 25 +- core/cmd/dms/init_service.go | 137 - core/cmd/dms/main.go | 1 - core/cmd/dms/main_distro.go | 1 - .../internal/config/embedded/niri-greeter.kdl | 17 - core/internal/config/niri.go | 3 - .../assets/apparmor/usr.bin.dms-greeter | 91 - core/internal/greeter/installer.go | 2697 ----------------- core/internal/greeter/installer_test.go | 350 --- core/internal/greeter/session_launcher.go | 200 -- .../internal/greeter/session_launcher_test.go | 57 - core/internal/greeter/user_cache_sync.go | 548 ---- core/internal/greeter/user_cache_sync_test.go | 81 - core/internal/headless/runner.go | 8 +- core/internal/pam/pam.go | 403 +-- core/internal/pam/pam_test.go | 365 +-- core/internal/qmlchecks/greeter_auth_test.go | 25 - core/internal/tui/views_dependencies.go | 11 +- core/internal/utils/greeter_setup.go | 40 + dank-qml-common | 2 +- distro/debian/dms-greeter/_service | 9 - distro/debian/dms-greeter/debian/changelog | 5 - distro/debian/dms-greeter/debian/control | 23 - distro/debian/dms-greeter/debian/copyright | 27 - distro/debian/dms-greeter/debian/postinst | 108 - distro/debian/dms-greeter/debian/postrm | 14 - distro/debian/dms-greeter/debian/rules | 50 - .../debian/dms-greeter/debian/source/format | 1 - .../debian/dms-greeter/debian/source/options | 1 - distro/fedora/dms-greeter.spec | 263 -- distro/nix/greeter.nix | 290 -- distro/nix/tests/default.nix | 8 - distro/nix/tests/greeter-niri-module.nix | 71 - distro/opensuse/dms-greeter.spec | 325 -- distro/scripts/copr-upload.sh | 5 +- distro/scripts/obs-status.sh | 5 +- distro/scripts/obs-upload.sh | 143 +- distro/scripts/ppa-build.sh | 16 - distro/scripts/ppa-status.sh | 5 +- distro/scripts/ppa-sync-plan.sh | 1 - distro/scripts/ppa-upload.sh | 16 +- distro/ubuntu/dms-greeter/debian/changelog | 5 - distro/ubuntu/dms-greeter/debian/control | 23 - distro/ubuntu/dms-greeter/debian/copyright | 27 - distro/ubuntu/dms-greeter/debian/postinst | 108 - distro/ubuntu/dms-greeter/debian/postrm | 14 - distro/ubuntu/dms-greeter/debian/rules | 67 - .../ubuntu/dms-greeter/debian/source/format | 1 - .../debian/source/include-binaries | 1 - .../ubuntu/dms-greeter/debian/source/options | 3 - distro/void/README.md | 25 - distro/void/srcpkgs/dms-greeter/INSTALL.msg | 15 - distro/void/srcpkgs/dms-greeter/template | 36 - flake.lock | 4 +- flake.nix | 5 +- quickshell/Common/CacheData.qml | 20 +- quickshell/Common/LayoutCodes.js | 107 - quickshell/Common/SessionData.qml | 59 +- quickshell/Common/SettingsData.qml | 50 +- quickshell/Common/Theme.qml | 114 +- quickshell/Common/settings/Processes.qml | 12 +- quickshell/DMSGreeter.qml | 9 - .../Modules/BlurredWallpaperBackground.qml | 7 +- quickshell/Modules/BlurredWallpaperLive.qml | 4 +- .../DankBar/Widgets/KeyboardLayoutName.qml | 2 +- quickshell/Modules/Greetd/GreetdEnv.js | 20 - quickshell/Modules/Greetd/GreetdMemory.qml | 162 - quickshell/Modules/Greetd/GreetdSettings.qml | 230 -- quickshell/Modules/Greetd/GreeterContent.qml | 2135 ------------- quickshell/Modules/Greetd/GreeterState.qml | 35 - quickshell/Modules/Greetd/GreeterSurface.qml | 34 - .../Modules/Greetd/GreeterUserPicker.qml | 240 -- .../Modules/Greetd/GreeterUserTheme.qml | 51 - quickshell/Modules/Greetd/README.md | 320 -- quickshell/Modules/Greetd/assets/dms-greeter | 499 --- .../Modules/Greetd/assets/dms-hypr.conf | 4 - quickshell/Modules/Greetd/assets/dms-hypr.lua | 8 - quickshell/Modules/Greetd/assets/dms-niri.kdl | 19 - .../Modules/Greetd/assets/greet-hyprland.sh | 11 - .../Modules/Greetd/assets/greet-niri.sh | 8 - .../Modules/Lock/CustomButtonKeyboard.qml | 44 - quickshell/Modules/Lock/Keyboard.qml | 369 --- .../Modules/Lock/KeyboardController.qml | 41 - quickshell/Modules/Lock/LockPowerMenu.qml | 827 ----- quickshell/Modules/Lock/LockScreenContent.qml | 3 +- quickshell/Modules/Settings/GreeterTab.qml | 112 +- quickshell/Modules/WallpaperBackground.qml | 7 +- quickshell/README.md | 1 - quickshell/Services/GreeterUsersService.qml | 163 - quickshell/Services/PortalService.qml | 50 - quickshell/Services/WeatherService.qml | 11 +- quickshell/scripts/i18nsync.py | 41 +- quickshell/scripts/qmllint-entrypoints.sh | 1 - quickshell/shell.qml | 10 - quickshell/systemd/sysusers-dms-greeter.conf | 3 - quickshell/systemd/tmpfiles-dms-greeter.conf | 3 - 106 files changed, 441 insertions(+), 14644 deletions(-) delete mode 100644 core/cmd/dms/commands_greeter_test.go delete mode 100644 core/cmd/dms/init_service.go delete mode 100644 core/internal/config/embedded/niri-greeter.kdl delete mode 100644 core/internal/greeter/assets/apparmor/usr.bin.dms-greeter delete mode 100644 core/internal/greeter/installer.go delete mode 100644 core/internal/greeter/installer_test.go delete mode 100644 core/internal/greeter/session_launcher.go delete mode 100644 core/internal/greeter/session_launcher_test.go delete mode 100644 core/internal/greeter/user_cache_sync.go delete mode 100644 core/internal/greeter/user_cache_sync_test.go delete mode 100644 core/internal/qmlchecks/greeter_auth_test.go create mode 100644 core/internal/utils/greeter_setup.go delete mode 100644 distro/debian/dms-greeter/_service delete mode 100644 distro/debian/dms-greeter/debian/changelog delete mode 100644 distro/debian/dms-greeter/debian/control delete mode 100644 distro/debian/dms-greeter/debian/copyright delete mode 100644 distro/debian/dms-greeter/debian/postinst delete mode 100644 distro/debian/dms-greeter/debian/postrm delete mode 100644 distro/debian/dms-greeter/debian/rules delete mode 100644 distro/debian/dms-greeter/debian/source/format delete mode 100644 distro/debian/dms-greeter/debian/source/options delete mode 100644 distro/fedora/dms-greeter.spec delete mode 100644 distro/nix/greeter.nix delete mode 100644 distro/nix/tests/greeter-niri-module.nix delete mode 100644 distro/opensuse/dms-greeter.spec delete mode 100644 distro/ubuntu/dms-greeter/debian/changelog delete mode 100644 distro/ubuntu/dms-greeter/debian/control delete mode 100644 distro/ubuntu/dms-greeter/debian/copyright delete mode 100755 distro/ubuntu/dms-greeter/debian/postinst delete mode 100755 distro/ubuntu/dms-greeter/debian/postrm delete mode 100755 distro/ubuntu/dms-greeter/debian/rules delete mode 100644 distro/ubuntu/dms-greeter/debian/source/format delete mode 100644 distro/ubuntu/dms-greeter/debian/source/include-binaries delete mode 100644 distro/ubuntu/dms-greeter/debian/source/options delete mode 100644 distro/void/srcpkgs/dms-greeter/INSTALL.msg delete mode 100644 distro/void/srcpkgs/dms-greeter/template delete mode 100644 quickshell/Common/LayoutCodes.js delete mode 100644 quickshell/DMSGreeter.qml delete mode 100644 quickshell/Modules/Greetd/GreetdEnv.js delete mode 100644 quickshell/Modules/Greetd/GreetdMemory.qml delete mode 100644 quickshell/Modules/Greetd/GreetdSettings.qml delete mode 100644 quickshell/Modules/Greetd/GreeterContent.qml delete mode 100644 quickshell/Modules/Greetd/GreeterState.qml delete mode 100644 quickshell/Modules/Greetd/GreeterSurface.qml delete mode 100644 quickshell/Modules/Greetd/GreeterUserPicker.qml delete mode 100644 quickshell/Modules/Greetd/GreeterUserTheme.qml delete mode 100644 quickshell/Modules/Greetd/README.md delete mode 100755 quickshell/Modules/Greetd/assets/dms-greeter delete mode 100644 quickshell/Modules/Greetd/assets/dms-hypr.conf delete mode 100644 quickshell/Modules/Greetd/assets/dms-hypr.lua delete mode 100644 quickshell/Modules/Greetd/assets/dms-niri.kdl delete mode 100755 quickshell/Modules/Greetd/assets/greet-hyprland.sh delete mode 100755 quickshell/Modules/Greetd/assets/greet-niri.sh delete mode 100644 quickshell/Modules/Lock/CustomButtonKeyboard.qml delete mode 100644 quickshell/Modules/Lock/Keyboard.qml delete mode 100644 quickshell/Modules/Lock/KeyboardController.qml delete mode 100644 quickshell/Modules/Lock/LockPowerMenu.qml delete mode 100644 quickshell/Services/GreeterUsersService.qml delete mode 100644 quickshell/systemd/sysusers-dms-greeter.conf delete mode 100644 quickshell/systemd/tmpfiles-dms-greeter.conf diff --git a/.github/workflows/nix-pr-check.yml b/.github/workflows/nix-pr-check.yml index eb540cedd..1ba7797eb 100644 --- a/.github/workflows/nix-pr-check.yml +++ b/.github/workflows/nix-pr-check.yml @@ -33,8 +33,6 @@ jobs: - name: Run NixOS service start test 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 run: nix build .#nixosTests.x86_64-linux.home-manager-module -L diff --git a/.github/workflows/run-copr.yml b/.github/workflows/run-copr.yml index d01ea328d..834e44eaa 100644 --- a/.github/workflows/run-copr.yml +++ b/.github/workflows/run-copr.yml @@ -4,14 +4,12 @@ on: workflow_dispatch: inputs: package: - description: 'Package to build (dms, dms-greeter, or both)' + description: 'Package to build' required: false default: 'dms' type: choice options: - dms - - dms-greeter - - both version: description: 'Versioning (e.g., 1.0.3, leave empty for latest release)' required: false @@ -31,11 +29,7 @@ jobs: id: set-packages run: | PACKAGE_INPUT="${{ github.event.inputs.package || 'dms' }}" - if [ "$PACKAGE_INPUT" = "both" ]; then - echo 'packages=["dms","dms-greeter"]' >> $GITHUB_OUTPUT - else - echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT - fi + echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT build-and-upload: needs: determine-packages @@ -163,8 +157,6 @@ jobs: PACKAGE="${{ matrix.package }}" if [ "$PACKAGE" = "dms" ]; then COPR_PROJECT="avengemedia/dms" - elif [ "$PACKAGE" = "dms-greeter" ]; then - COPR_PROJECT="avengemedia/danklinux" else echo "❌ Unknown package: $PACKAGE" exit 1 diff --git a/.github/workflows/run-obs.yml b/.github/workflows/run-obs.yml index 31ee2ae7e..87989ffb4 100644 --- a/.github/workflows/run-obs.yml +++ b/.github/workflows/run-obs.yml @@ -9,7 +9,6 @@ on: type: choice options: - dms - - dms-greeter - dms-git - all default: "dms" @@ -75,27 +74,12 @@ jobs: fi } - # Helper function to check dms-greeter stable tag - check_dms_greeter_stable() { - LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "") - local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:danklinux/dms-greeter/dms-greeter.spec" 2>/dev/null || echo "") - local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs | sed 's/^v//') - - if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "v$OBS_VERSION" ]]; then - echo "πŸ“‹ dms-greeter: Tag $LATEST_TAG already exists, skipping" - return 1 # No update needed - else - echo "πŸ“‹ dms-greeter: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})" - return 0 # Update needed - fi - } - # Main logic REBUILD="${{ github.event.inputs.rebuild_release }}" if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then - # Run from tag with no package specified - update both stable packages - echo "packages=dms dms-greeter" >> $GITHUB_OUTPUT + # Run from tag with no package specified - update stable package + echo "packages=dms" >> $GITHUB_OUTPUT VERSION="${GITHUB_REF#refs/tags/}" echo "version=$VERSION" >> $GITHUB_OUTPUT echo "has_updates=true" >> $GITHUB_OUTPUT @@ -121,7 +105,7 @@ jobs: echo "πŸ”„ Manual rebuild requested: $PKG (db$REBUILD)" 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=() if check_dms_stable; then PACKAGES_TO_UPDATE+=("dms") @@ -129,10 +113,6 @@ jobs: echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT fi fi - if check_dms_greeter_stable; then - PACKAGES_TO_UPDATE+=("dms-greeter") - [[ -n "$LATEST_TAG" ]] && echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT - fi if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT @@ -141,7 +121,7 @@ jobs: else echo "packages=" >> $GITHUB_OUTPUT echo "has_updates=false" >> $GITHUB_OUTPUT - echo "βœ“ Both packages up to date" + echo "βœ“ All packages up to date" fi elif [[ "$PKG" == "dms-git" ]]; then @@ -165,18 +145,6 @@ jobs: echo "has_updates=false" >> $GITHUB_OUTPUT fi - elif [[ "$PKG" == "dms-greeter" ]]; then - if check_dms_greeter_stable; then - echo "packages=$PKG" >> $GITHUB_OUTPUT - echo "has_updates=true" >> $GITHUB_OUTPUT - if [[ -n "$LATEST_TAG" ]]; then - echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT - fi - else - echo "packages=" >> $GITHUB_OUTPUT - echo "has_updates=false" >> $GITHUB_OUTPUT - fi - else # Unknown package - proceed anyway echo "packages=$PKG" >> $GITHUB_OUTPUT @@ -221,9 +189,9 @@ jobs: elif [[ -n "${{ github.event.inputs.package }}" ]]; then # 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 - 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 "") if [[ -n "$LATEST_TAG" ]]; then echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT @@ -289,7 +257,7 @@ jobs: echo " -- Avenge Media $CHANGELOG_DATE" } > "distro/debian/dms-git/debian/changelog" - - name: Update stable version (dms + dms-greeter) + - name: Update stable version (dms) if: steps.packages.outputs.version != '' run: | VERSION="${{ steps.packages.outputs.version }}" @@ -325,25 +293,12 @@ jobs: 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 $CHANGELOG_DATE" - } > "distro/debian/dms-greeter/debian/changelog" - echo "βœ“ Updated dms-greeter changelog to ${VERSION_NO_V}db1" - fi - # Update Debian _service files for packages in upload list (download_url paths) for service in distro/debian/*/_service; do if [[ -f "$service" ]]; then # Update tar_scm revision parameter (for dms-git) sed -i "s|v[0-9.]*|$VERSION|" "$service" - # Update download_url paths (for dms, dms-greeter stable) + # Update download_url paths (for dms stable) sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service" sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service" fi @@ -404,7 +359,7 @@ jobs: UPLOADED_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 for PKG in $PACKAGES; do echo "" @@ -507,9 +462,6 @@ jobs: dms-git) echo "- $STATUS_ICON **dms-git** ($STATUS_TEXT) β†’ [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY ;; - dms-greeter) - echo "- $STATUS_ICON **dms-greeter** ($STATUS_TEXT) β†’ [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:danklinux/dms-greeter)" >> $GITHUB_STEP_SUMMARY - ;; esac done echo "" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/run-ppa.yml b/.github/workflows/run-ppa.yml index 01a0b038a..58c5c2e1b 100644 --- a/.github/workflows/run-ppa.yml +++ b/.github/workflows/run-ppa.yml @@ -9,7 +9,6 @@ on: type: choice options: - dms - - dms-greeter - dms-git - all default: "dms" @@ -135,7 +134,6 @@ jobs: case "$PACKAGE" in dms) PPA_NAME="dms" ;; dms-git) PPA_NAME="dms-git" ;; - dms-greeter) PPA_NAME="danklinux" ;; *) echo "::error::Unknown package $PACKAGE"; exit 1 ;; esac @@ -150,4 +148,3 @@ jobs: echo "- **Target:** ${{ matrix.target }}" >> "$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 "- **DankLinux PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/run-xbps.yml b/.github/workflows/run-xbps.yml index 1195d95cd..51a796ead 100644 --- a/.github/workflows/run-xbps.yml +++ b/.github/workflows/run-xbps.yml @@ -27,11 +27,6 @@ on: type: boolean required: false default: true - build_greeter: - description: 'Build stable dms-greeter package' - type: boolean - required: false - default: true permissions: contents: read @@ -106,7 +101,6 @@ jobs: - name: Inject templates run: | 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/ - name: Enable unprivileged user namespaces (Ubuntu 24.04) @@ -180,19 +174,16 @@ jobs: mkdir -p r2-repo/current BUILD_DMS="${{ github.event.inputs.build_dms || 'true' }}" - BUILD_GREETER="${{ github.event.inputs.build_greeter || 'true' }}" BUILD_GIT="${{ github.event.inputs.build_git || 'true' }}" FORCE_REBUILD="${{ github.event.inputs.force_rebuild || 'false' }}" if [ "${{ github.event_name }}" = "schedule" ]; then BUILD_DMS="false" - BUILD_GREETER="false" BUILD_GIT="true" fi if [ "${{ github.event_name }}" = "release" ]; then BUILD_DMS="true" - BUILD_GREETER="true" BUILD_GIT="false" fi @@ -217,23 +208,20 @@ jobs: echo "=== Starting Builds ===" echo "DMS stable build enabled: $BUILD_DMS" - echo "Greeter stable build enabled: $BUILD_GREETER" echo "Git build enabled: $BUILD_GIT" echo "Force rebuild: $FORCE_REBUILD" cd void-packages - if [ -n "$RELEASE_VER" ] && { [ "$BUILD_DMS" = "true" ] || [ "$BUILD_GREETER" = "true" ]; }; then - echo "πŸ”§ Updating stable templates for $ARCHIVE_TAG" + if [ -n "$RELEASE_VER" ] && [ "$BUILD_DMS" = "true" ]; then + echo "πŸ”§ Updating stable template for $ARCHIVE_TAG" TARBALL="$(mktemp)" curl -fsSL -o "$TARBALL" "https://github.com/${{ github.repository }}/releases/download/${ARCHIVE_TAG}/dms-source.tar.gz" RELEASE_CHECKSUM="$(sha256sum "$TARBALL" | cut -d' ' -f1)" rm -f "$TARBALL" - for pkg in dms dms-greeter; do - sed -i "s/^version=.*/version=${RELEASE_VER}/" "srcpkgs/${pkg}/template" - sed -i "s/^checksum=.*/checksum=${RELEASE_CHECKSUM}/" "srcpkgs/${pkg}/template" - done + sed -i "s/^version=.*/version=${RELEASE_VER}/" srcpkgs/dms/template + sed -i "s/^checksum=.*/checksum=${RELEASE_CHECKSUM}/" srcpkgs/dms/template fi # 1. Build dms-git (development package) @@ -289,22 +277,6 @@ jobs: 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 run: | cd r2-repo/current diff --git a/README.md b/README.md index cb4ed133c..9673faae8 100644 --- a/README.md +++ b/README.md @@ -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. **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** 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) - **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 See component-specific documentation: diff --git a/core/README.md b/core/README.md index 9ea753ee3..099195d3c 100644 --- a/core/README.md +++ b/core/README.md @@ -78,7 +78,7 @@ Custom IPC via unix socket (JSON API) for shell communication. - `dms brightness [list|set]` - Control display/monitor brightness - `dms color pick` - Native color picker (see below) - `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 @@ -109,7 +109,7 @@ make test # Run tests **Distribution build:** ```bash -make dist # Build without update/greeter features +make dist # Build without update features ``` 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 ` | | Skip specific dependencies | | `--replace-configs ` | | Replace specific configuration files (mutually exclusive with `--replace-configs-all`) | | `--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. Configuration files are not replaced by default unless `--replace-configs` or `--replace-configs-all` is specified. diff --git a/core/cmd/dms/commands_auth.go b/core/cmd/dms/commands_auth.go index f0e020d9a..8214869f8 100644 --- a/core/cmd/dms/commands_auth.go +++ b/core/cmd/dms/commands_auth.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "os/exec" "path/filepath" "strings" @@ -15,7 +16,7 @@ import ( var authCmd = &cobra.Command{ Use: "auth", 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{ @@ -211,3 +212,31 @@ func syncAuthInTerminal(nonInteractive bool) error { shellCmd := shellSyncCmd + `; echo; echo "Authentication sync finished. Closing in 3 seconds..."; sleep 3` 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)") +} diff --git a/core/cmd/dms/commands_greeter.go b/core/cmd/dms/commands_greeter.go index 0d6746dc5..bef386f2d 100644 --- a/core/cmd/dms/commands_greeter.go +++ b/core/cmd/dms/commands_greeter.go @@ -1,1554 +1,48 @@ package main import ( - "bufio" - "context" - "encoding/json" "fmt" "os" "os/exec" - "os/user" - "path/filepath" - "strconv" - "strings" + "syscall" "github.com/AvengeMedia/DankMaterialShell/core/internal/distros" - "github.com/AvengeMedia/DankMaterialShell/core/internal/greeter" - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam" - "github.com/AvengeMedia/DankMaterialShell/core/internal/privesc" - "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" "github.com/spf13/cobra" - "golang.org/x/text/cases" - "golang.org/x/text/language" ) var greeterCmd = &cobra.Command{ - Use: "greeter", - Short: "Manage DMS greeter", - Long: "Manage DMS greeter (greetd)", -} - -var ( - greeterConfigSyncFn = greeter.SyncDMSConfigs - sharedAuthSyncFn = sharedpam.SyncAuthConfig - greeterIsNixOSFn = greeter.IsNixOS -) - -var greeterInstallCmd = &cobra.Command{ - Use: "install", - Short: "Install and configure DMS greeter", - Long: "Install greetd and configure it to use DMS as the greeter interface", - PreRunE: preRunGreeterMutation, - Run: func(cmd *cobra.Command, args []string) { - yes, _ := cmd.Flags().GetBool("yes") - term, _ := cmd.Flags().GetBool("terminal") - if term { - installCmd := "dms greeter install" - if yes { - installCmd += " --yes" - } - installCmd += "; echo; echo \"Install finished. Closing in 3 seconds...\"; sleep 3" - if err := runCommandInTerminal(installCmd); err != nil { - log.Fatalf("Error launching install in terminal: %v", err) - } - return + Use: "greeter", + Short: "Deprecated: moved to the standalone dms-greeter binary", + Long: "The greeter has moved to the standalone dms-greeter package.\nThis command forwards to 'dms-greeter' when it is installed.", + DisableFlagParsing: true, + SilenceUsage: true, + RunE: func(_ *cobra.Command, args []string) error { + binary, err := exec.LookPath("dms-greeter") + if err != nil { + return fmt.Errorf("'dms greeter' has moved to the standalone dms-greeter package.\n %s", greeterPackageInstallHint()) } - if err := installGreeter(yes); err != nil { - log.Fatalf("Error installing greeter: %v", err) + if isLegacyWrapperScript(binary) { + return fmt.Errorf("'dms greeter' has moved to the standalone dms-greeter package; %s is the old wrapper script.\n %s", binary, greeterPackageInstallHint()) } + fmt.Fprintln(os.Stderr, "warning: 'dms greeter' is deprecated; use 'dms-greeter' directly") + return syscall.Exec(binary, append([]string{"dms-greeter"}, args...), os.Environ()) }, } -var greeterSyncCmd = &cobra.Command{ - Use: "sync", - Short: "Sync DMS theme and settings with greeter", - Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen. Also updates a per-user cache slot at users// for multi-account greeter theme preview.\n\nUse --profile on secondary accounts to sync only your own users// slot without sudo or greetd changes.", - PreRunE: func(cmd *cobra.Command, args []string) error { - if err := rejectNixOSGreeterMutation(cmd); err != nil { - return err - } - profile, _ := cmd.Flags().GetBool("profile") - if profile { - return nil - } - return preRunPrivileged(cmd, args) - }, - Run: func(cmd *cobra.Command, args []string) { - yes, _ := cmd.Flags().GetBool("yes") - auth, _ := cmd.Flags().GetBool("auth") - local, _ := cmd.Flags().GetBool("local") - profile, _ := cmd.Flags().GetBool("profile") - autologinOnly, _ := cmd.Flags().GetBool("autologin") - term, _ := cmd.Flags().GetBool("terminal") - if term { - if err := syncInTerminal(yes, auth, local, profile, autologinOnly); err != nil { - log.Fatalf("Error launching sync in terminal: %v", err) - } - return - } - if autologinOnly { - if err := syncGreeterAutoLoginOnly(yes); err != nil { - log.Fatalf("Error syncing greeter auto-login: %v", err) - } - return - } - if err := syncGreeter(yes, auth, local, profile); err != nil { - log.Fatalf("Error syncing greeter: %v", err) - } - }, -} - -var greeterLaunchSessionCmd = &cobra.Command{ - Use: "launch-session", - Short: "Launch a remembered greeter session", - Hidden: true, - Run: func(cmd *cobra.Command, args []string) { - sessionID, _ := cmd.Flags().GetString("session-id") - fromMemory, _ := cmd.Flags().GetBool("from-memory") - cacheDir, _ := cmd.Flags().GetString("cache-dir") - - if fromMemory { - homeDir, err := os.UserHomeDir() - if err != nil { - log.Fatalf("failed to get user home directory: %v", err) - } - if err := greeter.LaunchSessionFromMemory(cacheDir, homeDir); err != nil { - log.Fatalf("failed to launch remembered greeter session: %v", err) - } - return - } - - if sessionID == "" { - log.Fatal("missing --session-id or --from-memory") - } - if err := greeter.LaunchSessionByID(sessionID); err != nil { - log.Fatalf("failed to launch greeter session %q: %v", sessionID, err) - } - }, -} - -func init() { - greeterSyncCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode: skip prompts, use defaults (for UI)") - greeterSyncCmd.Flags().BoolP("terminal", "t", false, "Run sync in a new terminal (for entering sudo password); terminal auto-closes when done") - greeterSyncCmd.Flags().BoolP("auth", "a", false, "Configure PAM for fingerprint and U2F (adds both if modules exist); overrides UI toggles") - greeterSyncCmd.Flags().BoolP("local", "l", false, "Developer mode: force greetd config to use a local DMS checkout path") - greeterSyncCmd.Flags().BoolP("profile", "p", false, "Sync only your per-user greeter slot (no sudo; for secondary accounts)") - greeterSyncCmd.Flags().Bool("autologin", false, "Apply only greeter auto-login on startup settings to greetd (no theme or auth sync)") - greeterLaunchSessionCmd.Flags().String("session-id", "", "Desktop session id to launch") - greeterLaunchSessionCmd.Flags().Bool("from-memory", false, "Resolve the session id from greeter memory") - greeterLaunchSessionCmd.Flags().String("cache-dir", greeter.GreeterCacheDir, "Greeter cache directory") -} - -var greeterEnableCmd = &cobra.Command{ - Use: "enable", - Short: "Enable DMS greeter in greetd config", - Long: "Configure greetd to use DMS as the greeter", - PreRunE: preRunGreeterMutation, - Run: func(cmd *cobra.Command, args []string) { - yes, _ := cmd.Flags().GetBool("yes") - term, _ := cmd.Flags().GetBool("terminal") - if term { - enableCmd := "dms greeter enable" - if yes { - enableCmd += " --yes" - } - enableCmd += "; echo; echo \"Enable finished. Closing in 3 seconds...\"; sleep 3" - if err := runCommandInTerminal(enableCmd); err != nil { - log.Fatalf("Error launching enable in terminal: %v", err) - } - return - } - if err := enableGreeter(yes); err != nil { - log.Fatalf("Error enabling greeter: %v", err) - } - }, -} - -var greeterStatusCmd = &cobra.Command{ - Use: "status", - Short: "Check greeter sync status", - Long: "Check the status of greeter installation and configuration sync", - Run: func(cmd *cobra.Command, args []string) { - if err := checkGreeterStatus(); err != nil { - log.Fatalf("Error checking greeter status: %v", err) - } - }, -} - -var greeterUninstallCmd = &cobra.Command{ - Use: "uninstall", - Short: "Remove DMS greeter configuration and restore previous display manager", - Long: "Disable greetd, remove DMS managed configs, and restore the system to its pre-DMS-greeter state", - PreRunE: preRunGreeterMutation, - Run: func(cmd *cobra.Command, args []string) { - yes, _ := cmd.Flags().GetBool("yes") - term, _ := cmd.Flags().GetBool("terminal") - if term { - uninstallCmd := "dms greeter uninstall" - if yes { - uninstallCmd += " --yes" - } - uninstallCmd += "; echo; echo \"Uninstall finished. Closing in 3 seconds...\"; sleep 3" - if err := runCommandInTerminal(uninstallCmd); err != nil { - log.Fatalf("Error launching uninstall in terminal: %v", err) - } - return - } - if err := uninstallGreeter(yes); err != nil { - log.Fatalf("Error uninstalling greeter: %v", err) - } - }, -} - -func init() { - greeterInstallCmd.Flags().BoolP("yes", "y", false, "Non-interactive: skip confirmation prompt") - greeterInstallCmd.Flags().BoolP("terminal", "t", false, "Run in a new terminal (for entering sudo password)") - greeterEnableCmd.Flags().BoolP("yes", "y", false, "Non-interactive: skip confirmation prompt") - greeterEnableCmd.Flags().BoolP("terminal", "t", false, "Run in a new terminal (for entering sudo password)") - greeterUninstallCmd.Flags().BoolP("yes", "y", false, "Non-interactive: skip confirmation prompt") - greeterUninstallCmd.Flags().BoolP("terminal", "t", false, "Run in a new terminal (for entering sudo password)") -} - -func rejectNixOSGreeterMutation(cmd *cobra.Command) error { - if !greeterIsNixOSFn() { - return nil - } - - return fmt.Errorf("dms %s is disabled on NixOS because the greeter is managed declaratively\nConfigure the DMS greeter in your NixOS module, then apply the change with your normal nixos-rebuild workflow", normalizeCommandSpec(cmd.CommandPath())) -} - -func preRunGreeterMutation(cmd *cobra.Command, args []string) error { - if err := rejectNixOSGreeterMutation(cmd); err != nil { - return err - } - return preRunPrivileged(cmd, args) -} - -func syncGreeterConfigsAndAuth(dmsPath, compositor string, logFunc func(string), options sharedpam.SyncAuthOptions, beforeAuth func()) error { - if err := greeterConfigSyncFn(dmsPath, compositor, logFunc, ""); err != nil { - return err - } - if beforeAuth != nil { - beforeAuth() - } - return sharedAuthSyncFn(logFunc, "", options) -} - -func installGreeter(nonInteractive bool) error { - fmt.Println("=== DMS Greeter Installation ===") - - logFunc := func(msg string) { - fmt.Println(msg) - } - - if !nonInteractive { - fmt.Print("\nThis will install greetd (if needed), configure the DMS greeter, and enable it. Continue? [Y/n]: ") - var response string - fmt.Scanln(&response) - if strings.ToLower(strings.TrimSpace(response)) == "n" || strings.ToLower(strings.TrimSpace(response)) == "no" { - fmt.Println("Aborted.") - return nil - } - fmt.Println() - } - - if err := greeter.EnsureGreetdInstalled(logFunc, ""); err != nil { - return err - } - - greeter.TryInstallGreeterPackage(logFunc, "") - if isPackageOnlyGreeterDistro() && !greeter.IsGreeterPackaged() { - return fmt.Errorf("dms-greeter must be installed from distro packages on this distribution. %s", packageInstallHint()) - } - if greeter.IsGreeterPackaged() && greeter.HasLegacyLocalGreeterWrapper() { - return fmt.Errorf("legacy manual wrapper detected at /usr/local/bin/dms-greeter; remove it before using packaged dms-greeter: sudo rm -f /usr/local/bin/dms-greeter") - } - - if isGreeterEnabled() { - fmt.Print("\nGreeter is already installed and configured. Re-run to re-sync settings and permissions? [Y/n]: ") - var response string - fmt.Scanln(&response) - response = strings.TrimSpace(strings.ToLower(response)) - if response == "n" || response == "no" { - fmt.Println("Run 'dms greeter sync' to re-sync theme and settings at any time.") - return nil - } - fmt.Println() - } - - fmt.Println("\nDetecting DMS installation...") - dmsPath, err := detectDMSPath() +func isLegacyWrapperScript(path string) bool { + file, err := os.Open(path) if err != nil { - return err + return false } - fmt.Printf("βœ“ Found DMS at: %s\n", dmsPath) - - fmt.Println("\nDetecting installed compositors...") - compositors := greeter.DetectCompositors() - if len(compositors) == 0 { - return fmt.Errorf("no supported compositors found (niri or Hyprland required)") + defer file.Close() + header := make([]byte, 2) + if _, err := file.Read(header); err != nil { + return false } - - var selectedCompositor string - if len(compositors) == 1 { - selectedCompositor = compositors[0] - fmt.Printf("βœ“ Found compositor: %s\n", selectedCompositor) - } else { - var err error - selectedCompositor, err = greeter.PromptCompositorChoice(compositors) - if err != nil { - return err - } - fmt.Printf("βœ“ Selected compositor: %s\n", selectedCompositor) - } - - fmt.Println("\nSetting up dms-greeter group and permissions...") - if err := greeter.SetupDMSGroup(logFunc, ""); err != nil { - return err - } - - fmt.Println("\nCopying greeter files...") - if err := greeter.CopyGreeterFiles(dmsPath, selectedCompositor, logFunc, ""); err != nil { - return err - } - - if greeter.IsAppArmorEnabled() { - fmt.Println("\nConfiguring AppArmor profile...") - if err := greeter.InstallAppArmorProfile(logFunc, ""); err != nil { - logFunc(fmt.Sprintf("⚠ AppArmor profile setup failed: %v", err)) - } - } - - fmt.Println("\nConfiguring greetd...") - greeterPathForConfig := "" - if !greeter.IsGreeterPackaged() { - greeterPathForConfig = dmsPath - } - if err := greeter.ConfigureGreetd(greeterPathForConfig, selectedCompositor, logFunc, ""); err != nil { - return err - } - - fmt.Println("\nSynchronizing DMS configurations...") - if err := syncGreeterConfigsAndAuth(dmsPath, selectedCompositor, logFunc, sharedpam.SyncAuthOptions{}, func() { - fmt.Println("\nConfiguring authentication...") - }); err != nil { - return err - } - - if err := ensureGraphicalTarget(); err != nil { - return err - } - - if err := handleConflictingDisplayManagers(); err != nil { - return err - } - - if err := ensureGreetdEnabled(); err != nil { - return err - } - - fmt.Println("\n=== Installation Complete ===") - fmt.Println("\nTo start the greeter now, run:") - fmt.Println(startGreeterHint()) - fmt.Println("\nOr reboot to see the greeter at next boot.") - - return nil + return string(header) == "#!" } -func uninstallGreeter(nonInteractive bool) error { - fmt.Println("=== DMS Greeter Uninstall ===") - - logFunc := func(msg string) { fmt.Println(msg) } - - if !isGreeterEnabled() { - fmt.Println("β„Ή DMS greeter is not currently configured in /etc/greetd/config.toml.") - fmt.Println(" Nothing to undo for greetd configuration.") - } - - if !nonInteractive { - fmt.Print("\nThis will:\n β€’ Stop and disable greetd\n β€’ Remove the DMS-managed greeter auth block\n β€’ Remove the DMS AppArmor profile\n β€’ Restore the most recent pre-DMS greetd config (if available)\n\nContinue? [y/N]: ") - var response string - fmt.Scanln(&response) - if strings.ToLower(strings.TrimSpace(response)) != "y" { - fmt.Println("Aborted.") - return nil - } - } - - fmt.Println("\nDisabling greetd...") - if isRunit() { - if err := disableRunitService("greetd"); err != nil { - fmt.Printf(" ⚠ Could not disable greetd: %v\n", err) - } else { - fmt.Println(" βœ“ greetd disabled") - } - } else if err := privesc.Run(context.Background(), "", "systemctl", "disable", "greetd"); err != nil { - fmt.Printf(" ⚠ Could not disable greetd: %v\n", err) - } else { - fmt.Println(" βœ“ greetd disabled") - } - - fmt.Println("\nRemoving DMS authentication configuration...") - if err := sharedpam.RemoveManagedGreeterPamBlock(logFunc, ""); err != nil { - fmt.Printf(" ⚠ PAM cleanup failed: %v\n", err) - } - - fmt.Println("\nRemoving DMS AppArmor profile...") - if err := greeter.UninstallAppArmorProfile(logFunc, ""); err != nil { - fmt.Printf(" ⚠ AppArmor cleanup failed: %v\n", err) - } - - fmt.Println("\nRestoring greetd configuration...") - if err := restorePreDMSGreetdConfig(""); err != nil { - fmt.Printf(" ⚠ Could not restore previous greetd config: %v\n", err) - fmt.Println(" You may need to manually edit /etc/greetd/config.toml.") - } - - fmt.Println("\nChecking for other display managers to re-enable...") - suggestDisplayManagerRestore(nonInteractive) - - fmt.Println("\n=== Uninstall Complete ===") - fmt.Println("\nReboot to complete the uninstallation and switch to your previous display manager.") - fmt.Println("To re-enable DMS greeter at any time, run: dms greeter enable") - - return nil -} - -func restorePreDMSGreetdConfig(sudoPassword string) error { - const configPath = "/etc/greetd/config.toml" - const backupGlob = "/etc/greetd/config.toml.backup-*" - - matches, _ := filepath.Glob(backupGlob) - - for i := 0; i < len(matches)-1; i++ { - for j := i + 1; j < len(matches); j++ { - if matches[j] > matches[i] { - matches[i], matches[j] = matches[j], matches[i] - } - } - } - - for _, candidate := range matches { - data, err := os.ReadFile(candidate) - if err != nil { - continue - } - if strings.Contains(string(data), "dms-greeter") { - continue - } - tmp, err := os.CreateTemp("", "greetd-restore-*") - if err != nil { - return fmt.Errorf("could not create temp file: %w", err) - } - tmpPath := tmp.Name() - defer os.Remove(tmpPath) - if _, err := tmp.Write(data); err != nil { - tmp.Close() - return err - } - tmp.Close() - - if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, configPath); err != nil { - return fmt.Errorf("failed to restore %s: %w", candidate, err) - } - if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", configPath); err != nil { - return err - } - fmt.Printf(" βœ“ Restored greetd config from %s\n", candidate) - return nil - } - - minimal := `[terminal] -vt = 1 - -# DMS greeter has been uninstalled. -# Configure a greeter command here or re-enable a display manager. -[default_session] -user = "greeter" -command = "agreety --cmd /bin/bash" -` - tmp, err := os.CreateTemp("", "greetd-minimal-*") - if err != nil { - return fmt.Errorf("could not create temp file: %w", err) - } - tmpPath := tmp.Name() - defer os.Remove(tmpPath) - if _, err := tmp.WriteString(minimal); err != nil { - tmp.Close() - return err - } - tmp.Close() - - if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, configPath); err != nil { - return fmt.Errorf("failed to write fallback greetd config: %w", err) - } - _ = privesc.Run(context.Background(), sudoPassword, "chmod", "644", configPath) - fmt.Println(" βœ“ Wrote minimal fallback greetd config (configure a greeter command manually if needed)") - return nil -} - -// suggestDisplayManagerRestore scans for installed DMs and re-enables one -func suggestDisplayManagerRestore(nonInteractive bool) { - knownDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"} - var found []string - for _, dm := range knownDMs { - if utils.CommandExists(dm) || isSystemdUnitInstalled(dm) { - found = append(found, dm) - } - } - if len(found) == 0 { - fmt.Println(" β„Ή No other display managers detected.") - fmt.Println(" You can install one (e.g. gdm, lightdm, sddm) and then run:") - fmt.Println(" sudo systemctl enable --now ") - return - } - - enableDM := func(dm string) { - fmt.Printf(" Enabling %s...\n", dm) - if isRunit() { - if err := enableRunitService(dm); err != nil { - fmt.Printf(" ⚠ Failed to enable %s: %v\n", dm, err) - } else { - fmt.Printf(" βœ“ %s enabled (linked into %s).\n", dm, runitServiceDir) - } - return - } - if err := privesc.Run(context.Background(), "", "systemctl", "enable", "--force", dm); err != nil { - fmt.Printf(" ⚠ Failed to enable %s: %v\n", dm, err) - } else { - fmt.Printf(" βœ“ %s enabled (will take effect on next boot).\n", dm) - } - } - - if len(found) == 1 || nonInteractive { - chosen := found[0] - if len(found) > 1 { - fmt.Printf(" β„Ή Multiple display managers found (%s); enabling %s automatically.\n", - strings.Join(found, ", "), chosen) - } else { - fmt.Printf(" β„Ή Found display manager: %s\n", chosen) - } - enableDM(chosen) - return - } - - fmt.Println(" β„Ή Found the following display managers:") - for i, dm := range found { - fmt.Printf(" %d) %s\n", i+1, dm) - } - fmt.Print(" Choose a number to re-enable (or press Enter to skip): ") - - scanner := bufio.NewScanner(os.Stdin) - if !scanner.Scan() { - return - } - input := strings.TrimSpace(scanner.Text()) - if input == "" { - fmt.Println(" Skipped. You can re-enable a display manager later with:") - fmt.Println(" sudo systemctl enable --now ") - return - } - - n, err := strconv.Atoi(input) - if err != nil || n < 1 || n > len(found) { - fmt.Printf(" Invalid selection %q β€” skipping.\n", input) - return - } - - enableDM(found[n-1]) -} - -func isSystemdUnitInstalled(unit string) bool { - if isRunit() { - return runitServiceInstalled(unit) - } - cmd := exec.Command("systemctl", "list-unit-files", unit+".service", "--no-legend", "--no-pager") - out, err := cmd.Output() - return err == nil && strings.Contains(string(out), unit) -} - -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)") -} - -func syncInTerminal(nonInteractive bool, forceAuth bool, local bool, profileOnly bool, autologinOnly bool) error { - syncFlags := make([]string, 0, 5) - if nonInteractive { - syncFlags = append(syncFlags, "--yes") - } - if forceAuth { - syncFlags = append(syncFlags, "--auth") - } - if local { - syncFlags = append(syncFlags, "--local") - } - if profileOnly { - syncFlags = append(syncFlags, "--profile") - } - if autologinOnly { - syncFlags = append(syncFlags, "--autologin") - } - shellSyncCmd := "dms greeter sync" - if len(syncFlags) > 0 { - shellSyncCmd += " " + strings.Join(syncFlags, " ") - } - var shellCmd string - if autologinOnly { - shellCmd = shellSyncCmd + `; echo; echo "Auto-login update finished. Closing in 3 seconds..."; sleep 3` - } else { - shellCmd = shellSyncCmd + `; echo; echo "Sync finished. Closing in 3 seconds..."; sleep 3` - } - return runCommandInTerminal(shellCmd) -} - -func resolveLocalWrapperShell() (string, error) { - for _, shellName := range []string{"bash", "sh"} { - shellPath, err := exec.LookPath(shellName) - if err == nil { - return shellPath, nil - } - } - return "", fmt.Errorf("could not find bash or sh in PATH for local greeter wrapper") -} - -func syncGreeterAutoLoginOnly(nonInteractive bool) error { - logFunc := func(msg string) { - fmt.Println(msg) - } - - homeDir, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("failed to get user home directory: %w", err) - } - - settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json") - cacheSettingsPath := filepath.Join(greeter.GreeterCacheDir, "settings.json") - enabled := false - for _, path := range []string{cacheSettingsPath, settingsPath} { - data, readErr := os.ReadFile(path) - if readErr != nil { - continue - } - var cfg struct { - GreeterAutoLogin bool `json:"greeterAutoLogin"` - } - if json.Unmarshal(data, &cfg) == nil { - enabled = cfg.GreeterAutoLogin - break - } - } - - fmt.Println("=== Greeter Auto-Login ===") - fmt.Println() - if enabled { - fmt.Println("Enabling auto-login on startup in greetd.") - fmt.Println("After your next reboot, DMS will skip the greeter password until you sign out.") - } else { - fmt.Println("Disabling auto-login on startup in greetd.") - fmt.Println("After your next reboot, you will enter your password at the greeter again.") - } - fmt.Println() - fmt.Println("Administrator (sudo) access is required to update /etc/greetd/config.toml.") - fmt.Println() - - return greeter.SyncGreeterAutoLoginOnly(logFunc, "") -} - -func syncGreeter(nonInteractive bool, forceAuth bool, local bool, profileOnly bool) error { - if profileOnly { - return syncGreeterProfileOnly(nonInteractive) - } - - if !nonInteractive { - fmt.Println("=== DMS Greeter Sync ===") - fmt.Println() - } - - logFunc := func(msg string) { - fmt.Println(msg) - } - - if !nonInteractive { - fmt.Println("Detecting DMS installation...") - } - var dmsPath string - var err error - if local { - dmsPath, err = resolveLocalDMSPath() - if err != nil { - return err - } - if !nonInteractive { - fmt.Printf("βœ“ Using local DMS path: %s\n", dmsPath) - } - } else { - dmsPath, err = detectDMSPath() - if err != nil { - return err - } - if !nonInteractive { - fmt.Printf("βœ“ Found DMS at: %s\n", dmsPath) - } - } - - if !isGreeterEnabled() { - if nonInteractive { - return fmt.Errorf("greeter is not enabled; run 'dms greeter install' or 'dms greeter enable' first") - } - fmt.Println("\n⚠ DMS greeter is not enabled in greetd config.") - fmt.Print("Would you like to enable it now? (Y/n): ") - - var response string - fmt.Scanln(&response) - response = strings.ToLower(strings.TrimSpace(response)) - - if response != "n" && response != "no" { - if err := enableGreeter(false); err != nil { - return err - } - } else { - return fmt.Errorf("greeter must be enabled before syncing") - } - } - - if greeter.IsGreeterPackaged() && greeter.HasLegacyLocalGreeterWrapper() { - return fmt.Errorf("legacy manual wrapper detected at /usr/local/bin/dms-greeter; remove it before using packaged dms-greeter: sudo rm -f /usr/local/bin/dms-greeter") - } - - cacheDir := greeter.GreeterCacheDir - if _, err := os.Stat(cacheDir); os.IsNotExist(err) { - logFunc("Cache directory not found β€” attempting to create it...") - } - - greeterGroup := greeter.DetectGreeterGroup() - greeterGroupExists := utils.HasGroup(greeterGroup) - if greeterGroupExists { - currentUser, err := user.Current() - if err != nil { - return fmt.Errorf("failed to get current user: %w", err) - } - - groupsCmd := exec.Command("groups", currentUser.Username) - groupsOutput, err := groupsCmd.Output() - if err != nil { - return fmt.Errorf("failed to check groups: %w", err) - } - - inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup) - if !inGreeterGroup { - if nonInteractive { - logFunc(fmt.Sprintf("⚠ Not yet in %s group β€” will be added during sync (logout/login required to take effect).", greeterGroup)) - } else { - fmt.Printf("\n⚠ Warning: You are not in the %s group.\n", greeterGroup) - fmt.Printf("Would you like to add your user to the %s group? (Y/n): ", greeterGroup) - - var response string - fmt.Scanln(&response) - response = strings.ToLower(strings.TrimSpace(response)) - - if response != "n" && response != "no" { - fmt.Printf("\nAdding user to %s group...\n", greeterGroup) - if err := privesc.Run(context.Background(), "", "usermod", "-aG", greeterGroup, currentUser.Username); err != nil { - return fmt.Errorf("failed to add user to %s group: %w", greeterGroup, err) - } - fmt.Printf("βœ“ User added to %s group\n", greeterGroup) - fmt.Println("⚠ You will need to log out and back in for the group change to take effect") - } else { - return fmt.Errorf("aborted: user must be in the greeter group before syncing") - } - } - } - } - - compositor := detectConfiguredCompositor() - if compositor == "" { - compositors := greeter.DetectCompositors() - switch len(compositors) { - case 0: - return fmt.Errorf("no supported compositors found") - case 1: - compositor = compositors[0] - if !nonInteractive { - fmt.Printf("βœ“ Using compositor: %s\n", compositor) - } - default: - if nonInteractive { - compositor = compositors[0] - break - } - var err error - compositor, err = promptCompositorChoice(compositors) - if err != nil { - return err - } - fmt.Printf("βœ“ Selected compositor: %s\n", compositor) - } - } else if !nonInteractive { - fmt.Printf("βœ“ Detected compositor from config: %s\n", compositor) - } - - if local { - localWrapperScript := filepath.Join(dmsPath, "Modules", "Greetd", "assets", "dms-greeter") - restoreWrapperOverride := func() {} - if info, statErr := os.Stat(localWrapperScript); statErr == nil && !info.IsDir() { - wrapperShell, shellErr := resolveLocalWrapperShell() - if shellErr != nil { - return shellErr - } - previousWrapperOverride, hadWrapperOverride := os.LookupEnv("DMS_GREETER_WRAPPER_CMD") - wrapperCmdOverride := wrapperShell + " " + localWrapperScript - _ = os.Setenv("DMS_GREETER_WRAPPER_CMD", wrapperCmdOverride) - restoreWrapperOverride = func() { - if hadWrapperOverride { - _ = os.Setenv("DMS_GREETER_WRAPPER_CMD", previousWrapperOverride) - } else { - _ = os.Unsetenv("DMS_GREETER_WRAPPER_CMD") - } - } - if !nonInteractive { - fmt.Printf("βœ“ Using local greeter wrapper script: %s\n", localWrapperScript) - } - } else if !nonInteractive { - fmt.Printf("β„Ή Local wrapper script not found at %s; using system wrapper.\n", localWrapperScript) - } - - fmt.Println("\nUpdating greetd command to use local DMS path...") - err := greeter.ConfigureGreetd(dmsPath, compositor, logFunc, "") - restoreWrapperOverride() - if err != nil { - return fmt.Errorf("failed to apply local greeter path: %w", err) - } - if !nonInteractive { - fmt.Println("β„Ή Local mode applies both DMS path override (-p) and local wrapper behavior when available.") - } - } else { - greeterPathForConfig := "" - if !greeter.IsGreeterPackaged() { - greeterPathForConfig = dmsPath - } - fmt.Println("\nUpdating greetd command...") - if err := greeter.ConfigureGreetd(greeterPathForConfig, compositor, logFunc, ""); err != nil { - return fmt.Errorf("failed to update greetd command: %w", err) - } - } - - fmt.Println("\nSetting up permissions and ACLs...") - greeter.RemediateStaleACLs(logFunc, "") - greeter.RemediateStaleAppArmor(logFunc, "") - if err := greeter.SetupDMSGroup(logFunc, ""); err != nil { - return err - } - if err := greeter.EnsureGreeterCacheDir(logFunc, ""); err != nil { - return fmt.Errorf("failed to ensure greeter cache directory at %s: %w\nRun: sudo mkdir -p %s && sudo chown root:%s %s && sudo chmod 2770 %s", cacheDir, err, cacheDir, greeterGroup, cacheDir, cacheDir) - } - - fmt.Println("\nSynchronizing DMS configurations...") - if err := syncGreeterConfigsAndAuth(dmsPath, compositor, logFunc, sharedpam.SyncAuthOptions{ - ForceGreeterAuth: forceAuth, - }, func() { - fmt.Println("\nConfiguring authentication...") - }); err != nil { - return err - } - - if greeter.IsAppArmorEnabled() { - fmt.Println("\nConfiguring AppArmor profile...") - if err := greeter.InstallAppArmorProfile(logFunc, ""); err != nil { - logFunc(fmt.Sprintf("⚠ AppArmor profile setup failed: %v", err)) - } - } - - fmt.Println("\n=== Sync Complete ===") - fmt.Println("\nYour theme, settings, and wallpaper configuration have been synced with the greeter.") - fmt.Println("Shared authentication settings were also checked and reconciled where needed.") - if forceAuth { - fmt.Println("Authentication has been configured for fingerprint and U2F (where modules exist).") - } - fmt.Println("The changes will be visible on the next login screen.") - - return nil -} - -func syncGreeterProfileOnly(nonInteractive bool) error { - logFunc := func(msg string) { - fmt.Println(msg) - } - if !nonInteractive { - fmt.Println("=== DMS Greeter Profile Sync ===") - fmt.Println() - fmt.Println("Syncing your personal greeter theme slot (no system changes)...") - } - if err := greeter.SyncUserProfileCache(logFunc); err != nil { - return err - } - if !nonInteractive { - fmt.Println("\n=== Profile Sync Complete ===") - fmt.Println("\nYour theme, wallpaper, and profile photo have been synced for the login screen.") - fmt.Println("Log out to preview your greeter look when selecting your account.") - } - return nil -} - -func hasDmsShellQml(dir string) bool { - info, err := os.Stat(filepath.Join(dir, "shell.qml")) - return err == nil && !info.IsDir() -} - -func resolveDMSLocalCandidate(path string) (string, bool) { - if path == "" { - return "", false - } - if hasDmsShellQml(path) { - abs, err := filepath.Abs(path) - if err != nil { - return path, true - } - return abs, true - } - - quickshellPath := filepath.Join(path, "quickshell") - if hasDmsShellQml(quickshellPath) { - abs, err := filepath.Abs(quickshellPath) - if err != nil { - return quickshellPath, true - } - return abs, true - } - - return "", false -} - -// detectDMSPath finds the QML tree the greeter copies from. On-disk installs -// win; with a packaged greeter the embedded UI is a valid copy source, but an -// extraction path must never be persisted into greetd config, so unpackaged -// setups still require a real install. -func detectDMSPath() (string, error) { - path, err := greeter.DetectDMSPath() - if err == nil { - return path, nil - } - if !greeter.IsGreeterPackaged() { - return "", err - } - if resolveErr := shellApp.ResolveConfig(nil, nil); resolveErr != nil { - return "", err - } - return shellApp.ConfigPath(), nil -} - -func resolveLocalDMSPath() (string, error) { - if override := strings.TrimSpace(os.Getenv("DMS_LOCAL_PATH")); override != "" { - if resolved, ok := resolveDMSLocalCandidate(override); ok { - return resolved, nil - } - return "", fmt.Errorf("DMS_LOCAL_PATH is set but does not point to a valid DMS quickshell path: %s", override) - } - - wd, err := os.Getwd() - if err != nil { - return "", fmt.Errorf("failed to get current directory: %w", err) - } - - dir := wd - for { - if resolved, ok := resolveDMSLocalCandidate(dir); ok { - return resolved, nil - } - - parent := filepath.Dir(dir) - if parent == dir { - break - } - dir = parent - } - - homeDir, err := os.UserHomeDir() - if err == nil && homeDir != "" { - for _, candidate := range []string{ - filepath.Join(homeDir, "dms"), - filepath.Join(homeDir, "DankMaterialShell"), - filepath.Join(homeDir, "dankmaterialshell"), - filepath.Join(homeDir, "projects", "dms"), - filepath.Join(homeDir, "src", "dms"), - } { - if resolved, ok := resolveDMSLocalCandidate(candidate); ok { - return resolved, nil - } - } - - if entries, readErr := os.ReadDir(homeDir); readErr == nil { - for _, entry := range entries { - if !entry.IsDir() { - continue - } - name := strings.ToLower(entry.Name()) - if !strings.Contains(name, "dms") && !strings.Contains(name, "dank") { - continue - } - if resolved, ok := resolveDMSLocalCandidate(filepath.Join(homeDir, entry.Name())); ok { - return resolved, nil - } - } - } - } - - configuredCommand := readDefaultSessionCommand("/etc/greetd/config.toml") - if pathOverride := extractGreeterPathOverrideFromCommand(configuredCommand); pathOverride != "" { - if resolved, ok := resolveDMSLocalCandidate(pathOverride); ok { - return resolved, nil - } - } - - return "", fmt.Errorf("could not locate a local DMS checkout from %s; run from repo root, set DMS_LOCAL_PATH=/absolute/path/to/repo, or configure greetd with -p /path/to/quickshell", wd) -} - -func disableDisplayManager(dmName string) (bool, error) { - if isRunit() { - if !runitServiceEnabled(dmName) { - return false, nil - } - fmt.Printf("\nDisabling %s (runit)...\n", dmName) - if err := disableRunitService(dmName); err != nil { - return false, fmt.Errorf("failed to disable %s: %w", dmName, err) - } - fmt.Printf(" βœ“ %s disabled (removed from %s)\n", dmName, runitServiceDir) - return true, nil - } - - state, err := getSystemdServiceState(dmName) - if err != nil { - return false, fmt.Errorf("failed to check %s state: %w", dmName, err) - } - - if !state.Exists { - return false, nil - } - - fmt.Printf("\nChecking %s...\n", dmName) - fmt.Printf(" Current state: enabled=%s\n", state.EnabledState) - - actionTaken := false - - if state.NeedsDisable { - var action, actionVerb string - switch state.EnabledState { - case "static": - fmt.Printf(" Masking %s (static service cannot be disabled)...\n", dmName) - action = "mask" - actionVerb = "masked" - default: - fmt.Printf(" Disabling %s...\n", dmName) - action = "disable" - actionVerb = "disabled" - } - - if err := privesc.Run(context.Background(), "", "systemctl", action, dmName); err != nil { - return actionTaken, fmt.Errorf("failed to disable/mask %s: %w", dmName, err) - } - - enabledState, shouldDisable, verifyErr := checkSystemdServiceEnabled(dmName) - if verifyErr != nil { - fmt.Printf(" ⚠ Warning: Could not verify %s was %s: %v\n", dmName, actionVerb, verifyErr) - } else if shouldDisable { - return actionTaken, fmt.Errorf("%s is still in state '%s' after %s operation", dmName, enabledState, actionVerb) - } else { - fmt.Printf(" βœ“ %s %s (now: %s)\n", cases.Title(language.English).String(actionVerb), dmName, enabledState) - } - - actionTaken = true - } else { - if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" { - fmt.Printf(" βœ“ %s is already masked\n", dmName) - } else { - fmt.Printf(" βœ“ %s is already disabled\n", dmName) - } - } - - return actionTaken, nil -} - -func ensureGreetdEnabled() error { - if isRunit() { - fmt.Println("\nEnabling greetd service (runit)...") - if !runitServiceInstalled("greetd") { - return fmt.Errorf("greetd service not found in %s. Please install greetd first", runitSvDir) - } - if greeter.IsVoidLinux() { - ensureVoidLogindGreeter("_greeter") - } else { - ensureRunitSeat("_greeter") - } - ensureGreetdPamRundir() - if err := enableRunitService("greetd"); err != nil { - return fmt.Errorf("failed to enable greetd: %w", err) - } - fmt.Printf(" βœ“ greetd enabled (%s)\n", runitServiceDir) - return nil - } - - fmt.Println("\nChecking greetd service status...") - - state, err := getSystemdServiceState("greetd") - if err != nil { - return fmt.Errorf("failed to check greetd state: %w", err) - } - - if !state.Exists { - return fmt.Errorf("greetd service not found. Please install greetd first") - } - - fmt.Printf(" Current state: %s\n", state.EnabledState) - - if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" { - fmt.Println(" Unmasking greetd...") - if err := privesc.Run(context.Background(), "", "systemctl", "unmask", "greetd"); err != nil { - return fmt.Errorf("failed to unmask greetd: %w", err) - } - fmt.Println(" βœ“ Unmasked greetd") - } - - if state.EnabledState == "enabled" || state.EnabledState == "enabled-runtime" { - fmt.Println(" Reasserting greetd as active display manager...") - } else { - fmt.Println(" Enabling greetd service...") - } - - if err := privesc.Run(context.Background(), "", "systemctl", "enable", "--force", "greetd"); err != nil { - return fmt.Errorf("failed to enable greetd: %w", err) - } - - enabledState, _, verifyErr := checkSystemdServiceEnabled("greetd") - if verifyErr != nil { - fmt.Printf(" ⚠ Warning: Could not verify greetd enabled state: %v\n", verifyErr) - } else { - switch enabledState { - case "enabled", "enabled-runtime", "static", "indirect", "alias": - fmt.Printf(" βœ“ greetd enabled (state: %s)\n", enabledState) - default: - return fmt.Errorf("greetd is still in state '%s' after enable operation", enabledState) - } - } - - return nil -} - -func ensureGraphicalTarget() error { - if isRunit() { - // runit has no targets; a supervised greetd service is the graphical - // login, so there is nothing to set here. - return nil - } - - getDefaultCmd := exec.Command("systemctl", "get-default") - currentTarget, err := getDefaultCmd.Output() - if err != nil { - fmt.Println("⚠ Warning: Could not detect current default systemd target") - return nil - } - - currentTargetStr := strings.TrimSpace(string(currentTarget)) - if currentTargetStr != "graphical.target" { - fmt.Printf("\nSetting graphical.target as default (current: %s)...\n", currentTargetStr) - if err := privesc.Run(context.Background(), "", "systemctl", "set-default", "graphical.target"); err != nil { - fmt.Println("⚠ Warning: Failed to set graphical.target as default") - fmt.Println(" Greeter may not start on boot. Run manually:") - fmt.Println(" sudo systemctl set-default graphical.target") - return nil - } - fmt.Println("βœ“ Set graphical.target as default") - } else { - fmt.Println("βœ“ Default target already set to graphical.target") - } - - return nil -} - -func handleConflictingDisplayManagers() error { - fmt.Println("\n=== Checking for Conflicting Display Managers ===") - - conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"} - - disabledAny := false - var errors []string - - for _, dm := range conflictingDMs { - actionTaken, err := disableDisplayManager(dm) - if err != nil { - errMsg := fmt.Sprintf("Failed to handle %s: %v", dm, err) - errors = append(errors, errMsg) - fmt.Printf(" ⚠⚠⚠ ERROR: %s\n", errMsg) - continue - } - if actionTaken { - disabledAny = true - } - } - - if len(errors) > 0 { - fmt.Println("\n╔════════════════════════════════════════════════════════════╗") - fmt.Println("β•‘ ⚠⚠⚠ ERRORS OCCURRED ⚠⚠⚠ β•‘") - fmt.Println("β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•") - fmt.Println("\nSome display managers could not be disabled:") - for _, err := range errors { - fmt.Printf(" βœ— %s\n", err) - } - fmt.Println("\nThis may prevent greetd from starting properly.") - fmt.Println("You may need to manually disable them before greetd will work.") - fmt.Println("\nManual commands to try:") - for _, dm := range conflictingDMs { - fmt.Printf(" sudo systemctl disable %s\n", dm) - fmt.Printf(" sudo systemctl mask %s\n", dm) - } - fmt.Print("\nContinue with greeter enablement anyway? (Y/n): ") - - var response string - fmt.Scanln(&response) - response = strings.ToLower(strings.TrimSpace(response)) - - if response == "n" || response == "no" { - return fmt.Errorf("aborted due to display manager conflicts") - } - fmt.Println("\nContinuing despite errors...") - } - - if !disabledAny && len(errors) == 0 { - fmt.Println("\nβœ“ No conflicting display managers found") - } else if disabledAny && len(errors) == 0 { - fmt.Println("\nβœ“ Successfully handled all conflicting display managers") - } - - return nil -} - -func enableGreeter(nonInteractive bool) error { - fmt.Println("=== DMS Greeter Enable ===") - fmt.Println() - - configPath := "/etc/greetd/config.toml" - if _, err := os.Stat(configPath); os.IsNotExist(err) { - return fmt.Errorf("greetd config not found at %s\nPlease install greetd first", configPath) - } else if err != nil { - return fmt.Errorf("failed to access greetd config at %s: %w", configPath, err) - } - - if greeter.IsGreeterPackaged() && greeter.HasLegacyLocalGreeterWrapper() { - return fmt.Errorf("legacy manual wrapper detected at /usr/local/bin/dms-greeter; remove it before using packaged dms-greeter: sudo rm -f /usr/local/bin/dms-greeter") - } - - configAlreadyCorrect := isGreeterEnabled() - configuredCompositor := detectConfiguredCompositor() - - logFunc := func(msg string) { - fmt.Println(msg) - } - greeterGroup := greeter.DetectGreeterGroup() - - if configAlreadyCorrect { - fmt.Println("βœ“ Greeter is already configured with dms-greeter") - if configuredCompositor != "" { - fmt.Printf("βœ“ Configured compositor: %s\n", configuredCompositor) - } - - fmt.Println("\nSetting up dms-greeter group and permissions...") - if err := greeter.SetupDMSGroup(logFunc, ""); err != nil { - return err - } - if err := greeter.EnsureGreeterCacheDir(logFunc, ""); err != nil { - fmt.Printf("⚠ Could not ensure cache directory: %v\n Run: sudo mkdir -p %s && sudo chown root:%s %s && sudo chmod 2770 %s\n", err, greeter.GreeterCacheDir, greeterGroup, greeter.GreeterCacheDir, greeter.GreeterCacheDir) - } - if err := greeter.EnsureVoidLogindGreetdCommand(logFunc, ""); err != nil { - return err - } - - if err := ensureGraphicalTarget(); err != nil { - return err - } - - if err := handleConflictingDisplayManagers(); err != nil { - return err - } - - if err := ensureGreetdEnabled(); err != nil { - return err - } - - fmt.Println("\n=== Enable Complete ===") - fmt.Println("\nGreeter configuration verified and system state corrected.") - fmt.Println("To start the greeter now, run:") - fmt.Println(startGreeterHint()) - fmt.Println("\nOr reboot to see the greeter at boot time.") - - return nil - } - - if !nonInteractive { - fmt.Print("\nThis will configure greetd to use the DMS greeter and may disable other display managers. Continue? [Y/n]: ") - var response string - fmt.Scanln(&response) - if strings.ToLower(strings.TrimSpace(response)) == "n" || strings.ToLower(strings.TrimSpace(response)) == "no" { - fmt.Println("Aborted.") - return nil - } - fmt.Println() - } - - fmt.Println("Detecting installed compositors...") - compositors := greeter.DetectCompositors() - - if utils.CommandExists("sway") { - compositors = append(compositors, "sway") - } - - if len(compositors) == 0 { - return fmt.Errorf("no supported compositors found (niri, Hyprland, or sway required)") - } - - var selectedCompositor string - if len(compositors) == 1 { - selectedCompositor = compositors[0] - fmt.Printf("βœ“ Found compositor: %s\n", selectedCompositor) - } else { - var err error - selectedCompositor, err = promptCompositorChoice(compositors) - if err != nil { - return err - } - fmt.Printf("βœ“ Selected compositor: %s\n", selectedCompositor) - } - - greeterPathForConfig := "" - if !greeter.IsGreeterPackaged() { - dmsPath, err := greeter.DetectDMSPath() - if err != nil { - return fmt.Errorf("failed to detect DMS path for manual greeter configuration: %w", err) - } - greeterPathForConfig = dmsPath - } - if err := greeter.ConfigureGreetd(greeterPathForConfig, selectedCompositor, logFunc, ""); err != nil { - return fmt.Errorf("failed to configure greetd: %w", err) - } - - fmt.Println("\nSetting up dms-greeter group and permissions...") - if err := greeter.SetupDMSGroup(logFunc, ""); err != nil { - return err - } - if err := greeter.EnsureGreeterCacheDir(logFunc, ""); err != nil { - fmt.Printf("⚠ Could not ensure cache directory: %v\n Run: sudo mkdir -p %s && sudo chown root:%s %s && sudo chmod 2770 %s\n", err, greeter.GreeterCacheDir, greeterGroup, greeter.GreeterCacheDir, greeter.GreeterCacheDir) - } - - if greeter.IsAppArmorEnabled() { - if err := greeter.InstallAppArmorProfile(logFunc, ""); err != nil { - logFunc(fmt.Sprintf("⚠ AppArmor profile setup failed: %v", err)) - } - } - - if err := ensureGraphicalTarget(); err != nil { - return err - } - - if err := handleConflictingDisplayManagers(); err != nil { - return err - } - - if err := ensureGreetdEnabled(); err != nil { - return err - } - - fmt.Println("\n=== Enable Complete ===") - fmt.Println("\nTo start the greeter now, run:") - fmt.Println(startGreeterHint()) - fmt.Println("\nOr reboot to see the greeter at boot time.") - - return nil -} - -func isGreeterEnabled() bool { - command := readDefaultSessionCommand("/etc/greetd/config.toml") - return command != "" && strings.Contains(command, "dms-greeter") -} - -func detectConfiguredCompositor() string { - command := strings.ToLower(readDefaultSessionCommand("/etc/greetd/config.toml")) - switch { - case strings.Contains(command, "--command niri"): - return "niri" - case strings.Contains(command, "--command hyprland"): - return "hyprland" - case strings.Contains(command, "--command sway"): - return "sway" - } - return "" -} - -func stripTomlComment(line string) string { - trimmed := strings.TrimSpace(line) - if idx := strings.Index(trimmed, "#"); idx >= 0 { - return strings.TrimSpace(trimmed[:idx]) - } - return trimmed -} - -func parseTomlSection(line string) (string, bool) { - trimmed := stripTomlComment(line) - if len(trimmed) < 3 || !strings.HasPrefix(trimmed, "[") || !strings.HasSuffix(trimmed, "]") { - return "", false - } - return strings.TrimSpace(trimmed[1 : len(trimmed)-1]), true -} - -func readDefaultSessionCommand(configPath string) string { - data, err := os.ReadFile(configPath) - if err != nil { - return "" - } - - inDefaultSession := false - for line := range strings.SplitSeq(string(data), "\n") { - if section, ok := parseTomlSection(line); ok { - inDefaultSession = section == "default_session" - continue - } - - if !inDefaultSession { - continue - } - - trimmed := stripTomlComment(line) - if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") { - continue - } - - parts := strings.SplitN(trimmed, "=", 2) - if len(parts) != 2 { - continue - } - - command := strings.Trim(strings.TrimSpace(parts[1]), `"`) - if command != "" { - return command - } - } - - return "" -} - -func explicitGreeterCacheDirFromCommand(command string) (string, bool) { - tokens := strings.Fields(command) - for i := 0; i < len(tokens); i++ { - token := strings.Trim(tokens[i], "\"") - if token == "--cache-dir" && i+1 < len(tokens) { - value := strings.Trim(tokens[i+1], "\"") - if value != "" { - return value, true - } - } - if strings.HasPrefix(token, "--cache-dir=") { - value := strings.TrimPrefix(token, "--cache-dir=") - value = strings.Trim(value, "\"") - if value != "" { - return value, true - } - } - } - return "", false -} - -const nixOSGreeterStateDir = "/var/lib/dms-greeter" - -func greeterStatusStateDir(command string, isNixOS bool) string { - if cacheDir, ok := explicitGreeterCacheDirFromCommand(command); ok { - return cacheDir - } - if isNixOS { - return nixOSGreeterStateDir - } - return greeter.GreeterCacheDir -} - -func extractGreeterWrapperFromCommand(command string) string { - if command == "" { - return "" - } - tokens := strings.Fields(command) - if len(tokens) == 0 { - return "" - } - wrapperIndex := 0 - if filepath.Base(strings.Trim(tokens[0], "\"")) == "env" { - wrapperIndex = 1 - for wrapperIndex < len(tokens) && strings.Contains(tokens[wrapperIndex], "=") { - wrapperIndex++ - } - } - if wrapperIndex >= len(tokens) { - return "" - } - - wrapper := strings.Trim(tokens[wrapperIndex], "\"") - if wrapper == "" { - return "" - } - if wrapperIndex+1 < len(tokens) { - next := strings.Trim(tokens[wrapperIndex+1], "\"") - if next != "" && (filepath.Base(wrapper) == "bash" || filepath.Base(wrapper) == "sh") && strings.Contains(filepath.Base(next), "dms-greeter") { - return fmt.Sprintf("%s (script: %s)", wrapper, next) - } - } - return wrapper -} - -func extractGreeterPathOverrideFromCommand(command string) string { - if command == "" { - return "" - } - tokens := strings.Fields(command) - for i := 0; i < len(tokens); i++ { - token := strings.Trim(tokens[i], "\"") - if (token == "-p" || token == "--path") && i+1 < len(tokens) { - return strings.Trim(tokens[i+1], "\"") - } - if strings.HasPrefix(token, "--path=") { - value := strings.TrimPrefix(token, "--path=") - value = strings.Trim(value, "\"") - if value != "" { - return value - } - } - } - return "" -} - -func parseManagedGreeterPamAuth(pamText string) (managed bool, fingerprint bool, u2f bool, legacy bool) { - return sharedpam.ParseManagedGreeterPamAuth(pamText) -} - -func packageInstallHint() string { +func greeterPackageInstallHint() string { osInfo, err := distros.GetOSInfo() if err != nil { return "Install package: dms-greeter" @@ -1572,590 +66,6 @@ func packageInstallHint() string { case distros.FamilyVoid: return "Install with 'sudo xbps-install -S dms-greeter' (requires DMS XBPS repo: echo 'repository=https://void.danklinux.com/dms/current' | sudo tee /etc/xbps.d/dms.conf)" default: - return "Run 'dms greeter install' to install greeter" + return "Install the dms-greeter package for your distribution" } } - -func systemPamManagerRemediationHint() 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 isPackageOnlyGreeterDistro() bool { - osInfo, err := distros.GetOSInfo() - if err != nil { - return false - } - config, exists := distros.Registry[osInfo.Distribution.ID] - if !exists { - return false - } - return config.Family == distros.FamilyDebian || - config.Family == distros.FamilySUSE || - config.Family == distros.FamilyUbuntu || - config.Family == distros.FamilyFedora || - config.Family == distros.FamilyArch || - config.Family == distros.FamilyVoid -} - -func promptCompositorChoice(compositors []string) (string, error) { - fmt.Println("\nMultiple compositors detected:") - for i, comp := range compositors { - fmt.Printf("%d) %s\n", i+1, comp) - } - - var response string - fmt.Print("Choose compositor for greeter: ") - fmt.Scanln(&response) - response = strings.TrimSpace(response) - - choice := 0 - fmt.Sscanf(response, "%d", &choice) - - if choice < 1 || choice > len(compositors) { - return "", fmt.Errorf("invalid choice") - } - - return compositors[choice-1], nil -} - -func checkGreeterStatus() error { - fmt.Println("=== DMS Greeter Status ===") - fmt.Println() - - if greeterIsNixOSFn() { - return checkNixOSGreeterStatus() - } - - homeDir, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("failed to get user home directory: %w", err) - } - - currentUser, err := user.Current() - if err != nil { - return fmt.Errorf("failed to get current user: %w", err) - } - - configPath := "/etc/greetd/config.toml" - configuredCommand := "" - allGood := true - fmt.Println("Greeter Configuration:") - if _, err := os.ReadFile(configPath); err == nil { - configuredCommand = readDefaultSessionCommand(configPath) - if configuredCommand != "" && strings.Contains(configuredCommand, "dms-greeter") { - fmt.Println(" βœ“ Greeter is enabled") - if wrapper := extractGreeterWrapperFromCommand(configuredCommand); wrapper != "" { - fmt.Printf(" Wrapper: %s\n", wrapper) - } - if pathOverride := extractGreeterPathOverrideFromCommand(configuredCommand); pathOverride != "" { - fmt.Printf(" DMS path override: %s\n", pathOverride) - } - - compositor := detectConfiguredCompositor() - switch compositor { - case "niri": - fmt.Println(" Compositor: niri") - case "hyprland": - fmt.Println(" Compositor: Hyprland") - case "sway": - fmt.Println(" Compositor: sway") - default: - fmt.Println(" Compositor: unknown") - } - } else { - fmt.Println(" βœ— Greeter is NOT enabled") - fmt.Println(" Run 'dms greeter enable' to enable it, or use the Activate button in Settings β†’ Greeter, then Sync.") - allGood = false - } - } else { - fmt.Println(" βœ— Greeter config not found") - fmt.Printf(" %s\n", packageInstallHint()) - allGood = false - } - - fmt.Println("\nGroup Membership:") - groupsCmd := exec.Command("groups", currentUser.Username) - groupsOutput, err := groupsCmd.Output() - if err != nil { - return fmt.Errorf("failed to check groups: %w", err) - } - - greeterGroup := greeter.DetectGreeterGroup() - inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup) - if inGreeterGroup { - fmt.Printf(" βœ“ User is in %s group\n", greeterGroup) - } else { - fmt.Printf(" βœ— User is NOT in %s group\n", greeterGroup) - fmt.Println(" Run 'dms greeter sync' to set up group membership and permissions") - } - - cacheDir := greeterStatusStateDir(configuredCommand, false) - fmt.Println("\nGreeter Cache Directory:") - fmt.Printf(" Effective cache dir: %s\n", cacheDir) - if cacheDir != greeter.GreeterCacheDir { - fmt.Printf(" ⚠ Non-default cache dir detected (default: %s)\n", greeter.GreeterCacheDir) - } - if stat, err := os.Stat(cacheDir); err == nil && stat.IsDir() { - fmt.Printf(" βœ“ %s exists\n", cacheDir) - requiredSubdirs := []string{".local/state", ".local/share", ".cache"} - missingSubdirs := false - for _, sub := range requiredSubdirs { - subPath := filepath.Join(cacheDir, sub) - if _, err := os.Stat(subPath); os.IsNotExist(err) { - fmt.Printf(" ⚠ Missing required subdir: %s\n", subPath) - missingSubdirs = true - } - } - if missingSubdirs { - fmt.Println(" Run 'dms greeter sync' to initialize the cache directory structure.") - allGood = false - } - } else { - fmt.Printf(" βœ— %s not found\n", cacheDir) - fmt.Printf(" %s\n", packageInstallHint()) - return nil - } - - fmt.Println("\nConfiguration Symlinks:") - colorSyncInfo, colorSyncErr := greeter.ResolveGreeterColorSyncInfo(homeDir) - if colorSyncErr != nil { - fmt.Printf(" βœ— Failed to resolve expected greeter color source: %v\n", colorSyncErr) - allGood = false - colorSyncInfo = greeter.GreeterColorSyncInfo{ - SourcePath: filepath.Join(homeDir, ".cache", "DankMaterialShell", "dms-colors.json"), - } - } - - colorThemeDesc := "Color theme" - if colorSyncInfo.UsesDynamicWallpaperOverride { - colorThemeDesc = "Color theme (greeter wallpaper override)" - } - - symlinks := []struct { - source string - target string - desc string - }{ - { - source: filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"), - target: filepath.Join(cacheDir, "settings.json"), - desc: "Settings", - }, - { - source: filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"), - target: filepath.Join(cacheDir, "session.json"), - desc: "Session state", - }, - { - source: colorSyncInfo.SourcePath, - target: filepath.Join(cacheDir, "colors.json"), - desc: colorThemeDesc, - }, - } - - for _, link := range symlinks { - targetInfo, err := os.Lstat(link.target) - if err != nil { - fmt.Printf(" βœ— %s: symlink not found at %s\n", link.desc, link.target) - allGood = false - continue - } - - if targetInfo.Mode()&os.ModeSymlink == 0 { - fmt.Printf(" βœ— %s: %s is not a symlink\n", link.desc, link.target) - allGood = false - continue - } - - linkDest, err := os.Readlink(link.target) - if err != nil { - fmt.Printf(" βœ— %s: failed to read symlink\n", link.desc) - allGood = false - continue - } - - if linkDest != link.source { - fmt.Printf(" βœ— %s: symlink points to wrong location\n", link.desc) - fmt.Printf(" Expected: %s\n", link.source) - fmt.Printf(" Got: %s\n", linkDest) - allGood = false - continue - } - - if _, err := os.Stat(link.source); os.IsNotExist(err) { - fmt.Printf(" ⚠ %s: symlink OK, but source file doesn't exist yet\n", link.desc) - fmt.Printf(" Will be created when you run DMS\n") - continue - } - - fmt.Printf(" βœ“ %s: synced correctly\n", link.desc) - } - - if colorSyncInfo.UsesDynamicWallpaperOverride { - fmt.Printf(" β„Ή Dynamic theme uses greeter override colors from %s\n", colorSyncInfo.SourcePath) - } - - fmt.Println("\nGreeter Wallpaper Override:") - overridePath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg") - if stat, err := os.Stat(overridePath); err == nil && !stat.IsDir() { - fmt.Printf(" βœ“ Override file present: %s\n", overridePath) - } else if os.IsNotExist(err) { - fmt.Println(" β„Ή Override file not present (desktop/session wallpaper fallback in effect)") - } else if err != nil { - fmt.Printf(" βœ— Could not inspect override file: %v\n", err) - allGood = false - } else { - fmt.Printf(" βœ— Override path is not a regular file: %s\n", overridePath) - allGood = false - } - - fmt.Println("\nGreeter PAM Authentication (DMS-managed block):") - if greeter.IsNixOS() { - fmt.Println(" β„Ή NixOS detected: PAM is managed by NixOS modules.") - fmt.Println(" Configure fingerprint/U2F via your greetd NixOS module (security.pam.services.greetd).") - fmt.Println() - if allGood && inGreeterGroup { - fmt.Println("βœ“ All checks passed! Greeter is properly configured.") - } else if !allGood { - fmt.Println("⚠ Some issues detected. Run 'dms greeter sync' to repair configuration.") - } else if !inGreeterGroup { - fmt.Printf("⚠ User is not in %s group. Run 'dms greeter sync' after adding group membership.\n", greeterGroup) - } - return nil - } - greetdPamPath := "/etc/pam.d/greetd" - pamData, err := os.ReadFile(greetdPamPath) - if err != nil { - fmt.Printf(" βœ— Failed to read %s: %v\n", greetdPamPath, err) - allGood = false - } else { - managed, managedFprint, managedU2f, legacyManaged := parseManagedGreeterPamAuth(string(pamData)) - if managed { - fmt.Println(" βœ“ Managed auth block present") - if managedFprint { - fmt.Println(" - fingerprint: enabled") - } else { - fmt.Println(" - fingerprint: disabled") - } - if managedU2f { - fmt.Println(" - security key (U2F): enabled") - } else { - fmt.Println(" - security key (U2F): disabled") - } - } else { - fmt.Println(" β„Ή No managed auth block present (DMS-managed fingerprint/U2F lines are disabled)") - } - if legacyManaged { - fmt.Println(" ⚠ Legacy unmanaged DMS PAM lines detected. Run 'dms auth sync' to normalize.") - allGood = false - } - enableFprintToggle, enableU2fToggle := false, false - if enableFprint, enableU2f, settingsErr := sharedpam.ReadGreeterAuthToggles(homeDir); settingsErr == nil { - enableFprintToggle = enableFprint - enableU2fToggle = enableU2f - } else { - fmt.Printf(" β„Ή Could not read greeter auth toggles from settings: %v\n", settingsErr) - } - - includedFprintFile := sharedpam.DetectIncludedPamModule(string(pamData), "pam_fprintd.so") - includedU2fFile := sharedpam.DetectIncludedPamModule(string(pamData), "pam_u2f.so") - fprintAvailableForCurrentUser := sharedpam.FingerprintAuthAvailableForCurrentUser() - - if managedFprint && includedFprintFile != "" { - fmt.Printf(" ⚠ pam_fprintd found in both DMS managed block and %s.\n", includedFprintFile) - fmt.Println(" Double fingerprint auth detected β€” run 'dms auth sync' to resolve.") - allGood = false - } - if managedU2f && includedU2fFile != "" { - fmt.Printf(" ⚠ pam_u2f found in both DMS managed block and %s.\n", includedU2fFile) - fmt.Println(" Double security-key auth detected β€” run 'dms auth sync' to resolve.") - allGood = false - } - - if includedFprintFile != "" && !managedFprint { - if enableFprintToggle { - fmt.Printf(" β„Ή Fingerprint auth is enabled via included %s.\n", includedFprintFile) - if fprintAvailableForCurrentUser { - fmt.Println(" DMS toggle is enabled, and effective auth is coming from the included PAM stack.") - } else { - fmt.Println(" No enrolled fingerprints detected for the current user; password auth remains the effective path.") - } - } else { - if fprintAvailableForCurrentUser { - fmt.Printf(" β„Ή Fingerprint auth is active via included %s while DMS fingerprint toggle is off.\n", includedFprintFile) - fmt.Println(" Password login will work but may be delayed while the fingerprint module runs first.") - fmt.Printf(" To eliminate the delay, %s\n", systemPamManagerRemediationHint()) - } else { - fmt.Printf(" β„Ή pam_fprintd is present via included %s, but no enrolled fingerprints were detected for user %s.\n", includedFprintFile, currentUser.Username) - fmt.Println(" Password auth remains the effective login path.") - } - } - } - if includedU2fFile != "" && !managedU2f { - if enableU2fToggle { - fmt.Printf(" β„Ή Security-key auth is enabled via included %s.\n", includedU2fFile) - fmt.Println(" DMS toggle is enabled, but effective auth is coming from the included PAM stack.") - } else { - fmt.Printf(" ⚠ Security-key auth is active via included %s while DMS security-key toggle is off.\n", includedU2fFile) - fmt.Printf(" %s\n", systemPamManagerRemediationHint()) - } - } - } - - fmt.Println("\nSecurity (AppArmor):") - if !greeter.IsAppArmorEnabled() { - fmt.Println(" β„Ή AppArmor not enabled") - } else { - fmt.Println(" β„Ή AppArmor is enabled") - - const appArmorProfilePath = "/etc/apparmor.d/usr.bin.dms-greeter" - if _, err := os.Stat(appArmorProfilePath); os.IsNotExist(err) { - fmt.Println(" ⚠ DMS AppArmor profile not installed") - fmt.Println(" Run 'dms greeter sync' to install it and prevent potential TTY fallback") - allGood = false - } else { - mode := appArmorProfileMode("dms-greeter") - if mode != "" { - fmt.Printf(" βœ“ DMS AppArmor profile installed (%s mode)\n", mode) - } else { - fmt.Println(" βœ“ DMS AppArmor profile installed") - } - } - - denialCount, denialSamples, denialErr := recentAppArmorGreeterDenials(3) - if denialErr != nil { - fmt.Printf(" β„Ή Could not inspect AppArmor denials automatically: %v\n", denialErr) - fmt.Println(" If greetd falls back to TTY, run: sudo journalctl -b -k | grep 'apparmor.*DENIED'") - } else if denialCount > 0 { - fmt.Printf(" ⚠ Found %d recent AppArmor denial(s) related to greeter runtime.\n", denialCount) - fmt.Println(" This can cause greetd fallback to TTY (for example: 'Failed to create stream fd: Permission denied').") - fmt.Println(" Review denials with: sudo journalctl -b -k | grep 'apparmor.*DENIED'") - fmt.Println(" Then refine the profile with: sudo aa-logprof") - for i, sample := range denialSamples { - fmt.Printf(" %d) %s\n", i+1, sample) - } - allGood = false - } else { - fmt.Println(" βœ“ No recent AppArmor denials detected for common greeter components") - } - } - - fmt.Println() - if allGood && inGreeterGroup { - fmt.Println("βœ“ All checks passed! Greeter is properly configured.") - } else if !allGood { - fmt.Println("⚠ Some issues detected. Run 'dms greeter sync' to repair configuration.") - } else if !inGreeterGroup { - fmt.Printf("⚠ User is not in %s group. Run 'dms greeter sync' after adding group membership.\n", greeterGroup) - } - - return nil -} - -func checkNixOSGreeterStatus() error { - const configPath = "/etc/greetd/config.toml" - - configuredCommand := readDefaultSessionCommand(configPath) - allGood := true - - fmt.Println("Greeter Configuration:") - switch { - case strings.Contains(configuredCommand, "dms-greeter"): - fmt.Println(" βœ“ DMS greeter command found") - if wrapper := extractGreeterWrapperFromCommand(configuredCommand); wrapper != "" { - fmt.Printf(" Wrapper: %s\n", wrapper) - } - case configuredCommand != "": - fmt.Println(" ⚠ greetd default session does not reference dms-greeter") - allGood = false - default: - fmt.Printf(" β„Ή No readable DMS command found in %s\n", configPath) - } - fmt.Println(" β„Ή NixOS manages greeter configuration declaratively; apply changes through your NixOS module.") - - stateDir := greeterStatusStateDir(configuredCommand, true) - fmt.Println("\nGreeter State Directory:") - fmt.Printf(" Effective state dir: %s\n", stateDir) - if stateDir == nixOSGreeterStateDir { - fmt.Println(" βœ“ Using the NixOS module state path") - } - if stat, err := os.Stat(stateDir); err == nil && stat.IsDir() { - fmt.Printf(" βœ“ %s exists\n", stateDir) - } else if os.IsNotExist(err) { - fmt.Printf(" βœ— %s not found\n", stateDir) - fmt.Println(" Rebuild your NixOS configuration after enabling the DMS greeter module.") - allGood = false - } else if err != nil { - fmt.Printf(" βœ— Could not inspect %s: %v\n", stateDir, err) - allGood = false - } else { - fmt.Printf(" βœ— %s is not a directory\n", stateDir) - allGood = false - } - - fmt.Println("\nDeclarative Configuration Files:") - configFiles := []struct { - name string - path string - }{ - {name: "Settings", path: filepath.Join(stateDir, "settings.json")}, - {name: "Session state", path: filepath.Join(stateDir, "session.json")}, - {name: "Color theme", path: filepath.Join(stateDir, "colors.json")}, - } - for _, configFile := range configFiles { - if stat, err := os.Stat(configFile.path); err == nil && !stat.IsDir() { - fmt.Printf(" βœ“ %s: %s\n", configFile.name, configFile.path) - } else if os.IsNotExist(err) { - fmt.Printf(" β„Ή %s not present (optional; configure configHome/configFiles in the NixOS module)\n", configFile.name) - } else if err != nil { - fmt.Printf(" ⚠ %s could not be inspected: %v\n", configFile.name, err) - } else { - fmt.Printf(" ⚠ %s path is not a regular file: %s\n", configFile.name, configFile.path) - } - } - - fmt.Println("\nGroup Membership:") - fmt.Println(" β„Ή User group membership is managed by NixOS and is not required for declarative theme copies.") - - fmt.Println("\nGreeter PAM Authentication:") - fmt.Println(" β„Ή PAM is managed by NixOS modules.") - fmt.Println(" Configure fingerprint/U2F through security.pam.services.greetd.") - - fmt.Println() - if allGood { - fmt.Println("βœ“ NixOS greeter state looks healthy and is managed declaratively.") - } else { - fmt.Println("⚠ Some issues detected. Update the DMS greeter module and rebuild NixOS; do not run 'dms greeter sync'.") - } - - return nil -} - -func recentAppArmorGreeterDenials(sampleLimit int) (int, []string, error) { - if sampleLimit <= 0 { - sampleLimit = 3 - } - if !utils.CommandExists("journalctl") { - return 0, nil, fmt.Errorf("journalctl not found") - } - - queries := [][]string{ - {"-b", "-k", "--no-pager", "-n", "2000", "-o", "cat"}, - {"-b", "--no-pager", "-n", "2000", "-o", "cat"}, - } - - seen := make(map[string]bool) - samples := make([]string, 0, sampleLimit) - total := 0 - var lastErr error - successfulQuery := false - - for _, query := range queries { - cmd := exec.Command("journalctl", query...) - output, err := cmd.CombinedOutput() - if err != nil { - lastErr = err - continue - } - successfulQuery = true - total += collectGreeterAppArmorDenials(string(output), seen, &samples, sampleLimit) - } - - if !successfulQuery && lastErr != nil { - return 0, nil, lastErr - } - - return total, samples, nil -} - -func collectGreeterAppArmorDenials(text string, seen map[string]bool, samples *[]string, sampleLimit int) int { - count := 0 - for _, rawLine := range strings.Split(text, "\n") { - line := strings.TrimSpace(rawLine) - if line == "" || !isGreeterRelatedAppArmorDenial(line) { - continue - } - if seen[line] { - continue - } - seen[line] = true - count++ - if len(*samples) < sampleLimit { - *samples = append(*samples, line) - } - } - return count -} - -func isGreeterRelatedAppArmorDenial(line string) bool { - lower := strings.ToLower(line) - if !strings.Contains(lower, "apparmor") || !strings.Contains(lower, "denied") { - return false - } - - greeterTokens := []string{ - "dms-greeter", - "/usr/bin/dms-greeter", - "greetd", - "quickshell", - "/usr/bin/qs", - "/usr/bin/quickshell", - "niri", - "hyprland", - "sway", - "mango", - "miracle", - "labwc", - "pipewire", - "wireplumber", - "stream fd", - } - - for _, token := range greeterTokens { - if strings.Contains(lower, token) { - return true - } - } - return false -} - -// appArmorProfileMode returns "complain", "enforce", or "" for a named AppArmor profile. -func appArmorProfileMode(profileName string) string { - data, err := os.ReadFile("/sys/kernel/security/apparmor/profiles") - if err != nil { - return "" - } - for _, line := range strings.Split(string(data), "\n") { - line = strings.TrimSpace(line) - if !strings.Contains(line, profileName) { - continue - } - lower := strings.ToLower(line) - if strings.Contains(lower, "(complain)") { - return "complain" - } - if strings.Contains(lower, "(enforce)") { - return "enforce" - } - if strings.Contains(lower, "(kill)") { - return "kill" - } - } - return "" -} diff --git a/core/cmd/dms/commands_greeter_test.go b/core/cmd/dms/commands_greeter_test.go deleted file mode 100644 index dd087cad5..000000000 --- a/core/cmd/dms/commands_greeter_test.go +++ /dev/null @@ -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) - } -} diff --git a/core/cmd/dms/commands_setup.go b/core/cmd/dms/commands_setup.go index d6a9b6856..69bab36d0 100644 --- a/core/cmd/dms/commands_setup.go +++ b/core/cmd/dms/commands_setup.go @@ -11,7 +11,6 @@ import ( "github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "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/privesc" "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" @@ -205,7 +204,12 @@ func detectTerminal() (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) { case 0: @@ -214,11 +218,20 @@ func detectCompositorForSetup() (string, error) { return strings.ToLower(compositors[0]), nil } - selected, err := greeter.PromptCompositorChoice(compositors) - if err != nil { - return "", err + fmt.Println("Multiple compositors detected:") + for i, compositor := range compositors { + 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 { diff --git a/core/cmd/dms/init_service.go b/core/cmd/dms/init_service.go deleted file mode 100644 index d1e92a9dc..000000000 --- a/core/cmd/dms/init_service.go +++ /dev/null @@ -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" -} diff --git a/core/cmd/dms/main.go b/core/cmd/dms/main.go index f1e5914be..b27fa30d8 100644 --- a/core/cmd/dms/main.go +++ b/core/cmd/dms/main.go @@ -12,7 +12,6 @@ import ( var Version = "dev" func init() { - greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd) authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) updateCmd.AddCommand(updateCheckCmd) diff --git a/core/cmd/dms/main_distro.go b/core/cmd/dms/main_distro.go index 2897085ce..df5592df8 100644 --- a/core/cmd/dms/main_distro.go +++ b/core/cmd/dms/main_distro.go @@ -12,7 +12,6 @@ import ( var Version = "dev" func init() { - greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd) authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) diff --git a/core/internal/config/embedded/niri-greeter.kdl b/core/internal/config/embedded/niri-greeter.kdl deleted file mode 100644 index 5653a16e8..000000000 --- a/core/internal/config/embedded/niri-greeter.kdl +++ /dev/null @@ -1,17 +0,0 @@ -hotkey-overlay { - skip-at-startup -} - -environment { - DMS_RUN_GREETER "1" -} - -gestures { - hot-corners { - off - } -} - -layout { - background-color "#000000" -} diff --git a/core/internal/config/niri.go b/core/internal/config/niri.go index 01801ca09..e68f77e03 100644 --- a/core/internal/config/niri.go +++ b/core/internal/config/niri.go @@ -16,6 +16,3 @@ var NiriAlttabConfig string //go:embed embedded/niri-binds.kdl var NiriBindsConfig string - -//go:embed embedded/niri-greeter.kdl -var NiriGreeterConfig string diff --git a/core/internal/greeter/assets/apparmor/usr.bin.dms-greeter b/core/internal/greeter/assets/apparmor/usr.bin.dms-greeter deleted file mode 100644 index 7c42f9ec3..000000000 --- a/core/internal/greeter/assets/apparmor/usr.bin.dms-greeter +++ /dev/null @@ -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 - -profile dms-greeter /usr/bin/dms-greeter flags=(complain) { - #include - #include - - # 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"), -} diff --git a/core/internal/greeter/installer.go b/core/internal/greeter/installer.go deleted file mode 100644 index 5767db45c..000000000 --- a/core/internal/greeter/installer.go +++ /dev/null @@ -1,2697 +0,0 @@ -package greeter - -import ( - "bufio" - "context" - _ "embed" - "encoding/json" - "errors" - "fmt" - "os" - "os/exec" - "os/user" - "path/filepath" - "strings" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/config" - "github.com/AvengeMedia/DankMaterialShell/core/internal/distros" - "github.com/AvengeMedia/DankMaterialShell/core/internal/matugen" - sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam" - "github.com/AvengeMedia/DankMaterialShell/core/internal/privesc" - "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" - "github.com/sblinch/kdl-go" - "github.com/sblinch/kdl-go/document" -) - -var appArmorProfileData []byte - -const appArmorProfileDest = "/etc/apparmor.d/usr.bin.dms-greeter" - -const GreeterCacheDir = "/var/cache/dms-greeter" - -const ( - runitSvDir = "/etc/sv" - runitServiceDir = "/var/service" -) - -func DetectDMSPath() (string, error) { - return config.LocateDMSConfig() -} - -// IsNixOS returns true when running on NixOS, which manages PAM configs through -// its module system. The DMS PAM managed block won't be written on NixOS. -func IsNixOS() bool { - _, err := os.Stat("/etc/NIXOS") - return err == nil -} - -func IsVoidLinux() bool { - osInfo, err := distros.GetOSInfo() - if err != nil { - return false - } - config, exists := distros.Registry[osInfo.Distribution.ID] - return exists && config.Family == distros.FamilyVoid -} - -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(filepath.Join(runitSvDir, name)) - return err == nil && fi.IsDir() -} - -func runitServiceEnabled(name string) bool { - _, err := os.Lstat(filepath.Join(runitServiceDir, name)) - return err == nil -} - -func enableRunitService(name, sudoPassword 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(), sudoPassword, "ln", "-sf", - filepath.Join(runitSvDir, name), filepath.Join(runitServiceDir, name)) -} - -func disableRunitService(name, sudoPassword string) error { - if !runitServiceEnabled(name) { - return nil - } - return privesc.Run(context.Background(), sudoPassword, "rm", "-f", - filepath.Join(runitServiceDir, name)) -} - -func ensureRunitSeat(greeterUser, sudoPassword string, logFunc func(string)) { - if runitServiceInstalled("seatd") { - if err := enableRunitService("seatd", sudoPassword); err != nil { - logFunc(fmt.Sprintf("⚠ could not enable seatd: %v", err)) - } else { - logFunc("βœ“ seatd enabled") - } - } else { - logFunc("⚠ seatd not installed β€” the greeter compositor needs it for GPU/seat access") - } - - if err := privesc.Run(context.Background(), sudoPassword, "usermod", "-aG", "_seatd,video,input", greeterUser); err != nil { - logFunc(fmt.Sprintf("⚠ could not add %s to seat groups: %v", greeterUser, err)) - } else { - logFunc(fmt.Sprintf("βœ“ %s added to seat groups (_seatd, video, input)", greeterUser)) - } -} - -// VoidGreetdRunScript orders greetd after dbus/elogind so the first greeter session can't race elogind's runtime-dir setup. -const VoidGreetdRunScript = `#!/bin/sh -sv check dbus >/dev/null || exit 1 -sv check elogind >/dev/null || exit 1 -exec greetd -` - -// EnsureVoidGreetdRunScript rewrites /etc/sv/greetd/run with dbus/elogind ordering (greetd updates restore stock; enable re-asserts). -func EnsureVoidGreetdRunScript(logFunc func(string), sudoPassword string) { - const runPath = "/etc/sv/greetd/run" - if data, err := os.ReadFile(runPath); err == nil && strings.Contains(string(data), "sv check elogind") { - logFunc("βœ“ greetd run script already waits for elogind") - return - } - script := fmt.Sprintf("cat > %s <<'EOF'\n%sEOF\nchmod 755 %s", runPath, VoidGreetdRunScript, runPath) - if err := privesc.Run(context.Background(), sudoPassword, "sh", "-c", script); err != nil { - logFunc(fmt.Sprintf("⚠ could not update %s: %v", runPath, err)) - return - } - logFunc("βœ“ greetd run script now waits for dbus/elogind") -} - -// ensureVoidLogindGreeter configures the elogind-backed greeter on Void. -func ensureVoidLogindGreeter(greeterUser, sudoPassword string, logFunc func(string)) { - for _, service := range []string{"dbus", "elogind"} { - if err := enableRunitService(service, sudoPassword); err != nil { - logFunc(fmt.Sprintf("⚠ could not enable %s: %v", service, err)) - } else { - logFunc(fmt.Sprintf("βœ“ %s enabled", service)) - } - } - EnsureVoidGreetdRunScript(logFunc, sudoPassword) - if runitServiceEnabled("seatd") { - if err := disableRunitService("seatd", sudoPassword); err != nil { - logFunc(fmt.Sprintf("⚠ could not disable seatd: %v", err)) - } else { - logFunc("βœ“ seatd disabled (elogind manages the seat)") - } - } - if err := privesc.Run(context.Background(), sudoPassword, "usermod", "-aG", "video,input", greeterUser); err != nil { - logFunc(fmt.Sprintf("⚠ could not add %s to video/input groups: %v", greeterUser, err)) - } else { - logFunc(fmt.Sprintf("βœ“ %s added to video/input groups (elogind manages the seat)", greeterUser)) - } -} - -func ensureGreetdPamRundir(sudoPassword string, logFunc func(string)) { - const pamPath = "/etc/pam.d/greetd" - data, err := os.ReadFile(pamPath) - if err != nil { - logFunc(fmt.Sprintf("⚠ could not read %s: %v", pamPath, err)) - return - } - if strings.Contains(string(data), "pam_rundir") { - logFunc("βœ“ pam_rundir already present in greetd PAM") - return - } - - line := "session optional pam_rundir.so" - if err := privesc.Run(context.Background(), sudoPassword, "sh", "-c", - fmt.Sprintf("printf '%%s\\n' %q >> %s", line, pamPath)); err != nil { - logFunc(fmt.Sprintf("⚠ could not add pam_rundir to %s: %v", pamPath, err)) - return - } - logFunc("βœ“ pam_rundir added to greetd PAM (provides XDG_RUNTIME_DIR for the session)") -} - -func DetectGreeterGroup() string { - data, err := os.ReadFile("/etc/group") - if err != nil { - fmt.Fprintln(os.Stderr, "⚠ Warning: could not read /etc/group, defaulting to greeter") - return "greeter" - } - - if group, found := utils.FindGroupData(string(data), "greeter", "greetd", "_greeter"); found { - return group - } - - fmt.Fprintln(os.Stderr, "⚠ Warning: no greeter group found in /etc/group, defaulting to greeter") - return "greeter" -} - -func hasPasswdUser(passwdData, user string) bool { - prefix := user + ":" - for line := range strings.SplitSeq(passwdData, "\n") { - if strings.HasPrefix(line, prefix) { - return true - } - } - return false -} - -func findPasswdUser(passwdData string, candidates ...string) (string, bool) { - for _, candidate := range candidates { - if hasPasswdUser(passwdData, candidate) { - return candidate, true - } - } - return "", false -} - -func stripTomlComment(line string) string { - trimmed := strings.TrimSpace(line) - if idx := strings.Index(trimmed, "#"); idx >= 0 { - return strings.TrimSpace(trimmed[:idx]) - } - return trimmed -} - -func parseTomlSection(line string) (string, bool) { - trimmed := stripTomlComment(line) - if len(trimmed) < 3 || !strings.HasPrefix(trimmed, "[") || !strings.HasSuffix(trimmed, "]") { - return "", false - } - return strings.TrimSpace(trimmed[1 : len(trimmed)-1]), true -} - -func extractDefaultSessionUser(configContent string) string { - inDefaultSession := false - for line := range strings.SplitSeq(configContent, "\n") { - if section, ok := parseTomlSection(line); ok { - inDefaultSession = section == "default_session" - continue - } - - if !inDefaultSession { - continue - } - - trimmed := stripTomlComment(line) - if !strings.HasPrefix(trimmed, "user =") && !strings.HasPrefix(trimmed, "user=") { - continue - } - - parts := strings.SplitN(trimmed, "=", 2) - if len(parts) != 2 { - continue - } - user := strings.Trim(strings.TrimSpace(parts[1]), `"`) - if user != "" { - return user - } - } - - return "" -} - -func upsertDefaultSession(configContent, greeterUser, command string) string { - lines := strings.Split(configContent, "\n") - var out []string - - inDefaultSession := false - foundDefaultSession := false - defaultSessionUserSet := false - defaultSessionCommandSet := false - - appendDefaultSessionFields := func() { - if !defaultSessionUserSet { - out = append(out, fmt.Sprintf(`user = "%s"`, greeterUser)) - } - if !defaultSessionCommandSet { - out = append(out, command) - } - } - - for _, line := range lines { - if section, ok := parseTomlSection(line); ok { - if inDefaultSession { - appendDefaultSessionFields() - } - - inDefaultSession = section == "default_session" - if inDefaultSession { - foundDefaultSession = true - defaultSessionUserSet = false - defaultSessionCommandSet = false - } - - out = append(out, line) - continue - } - - if inDefaultSession { - trimmed := stripTomlComment(line) - if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") { - out = append(out, fmt.Sprintf(`user = "%s"`, greeterUser)) - defaultSessionUserSet = true - continue - } - - if strings.HasPrefix(trimmed, "command =") || strings.HasPrefix(trimmed, "command=") { - if !defaultSessionCommandSet { - out = append(out, command) - defaultSessionCommandSet = true - } - continue - } - } - - out = append(out, line) - } - - if inDefaultSession { - appendDefaultSessionFields() - } - - if !foundDefaultSession { - if len(out) > 0 && strings.TrimSpace(out[len(out)-1]) != "" { - out = append(out, "") - } - out = append(out, "[default_session]") - out = append(out, fmt.Sprintf(`user = "%s"`, greeterUser)) - out = append(out, command) - } - - return strings.Join(out, "\n") -} - -func removeTomlSection(configContent, sectionName string) string { - lines := strings.Split(configContent, "\n") - var out []string - inSection := false - - for _, line := range lines { - if section, ok := parseTomlSection(line); ok { - inSection = section == sectionName - if inSection { - continue - } - out = append(out, line) - continue - } - - if inSection { - continue - } - - out = append(out, line) - } - - result := strings.TrimRight(strings.Join(out, "\n"), "\n") - if result != "" { - result += "\n" - } - return result -} - -func stripDesktopExecCodes(execLine string) string { - fields := strings.Fields(execLine) - cleaned := make([]string, 0, len(fields)) - for _, field := range fields { - if strings.HasPrefix(field, "%") { - continue - } - cleaned = append(cleaned, field) - } - return strings.Join(cleaned, " ") -} - -func shellQuote(value string) string { - return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'" -} - -func stableDMSCommand() string { - for _, candidate := range []string{"/usr/bin/dms", "/usr/local/bin/dms"} { - info, err := os.Stat(candidate) - if err == nil && !info.IsDir() && info.Mode()&0o111 != 0 { - return candidate - } - } - return "dms" -} - -func formatInitialSessionCommand(cacheDir string) string { - cacheDir = strings.TrimSpace(cacheDir) - if cacheDir == "" { - return `command = ""` - } - launcher := fmt.Sprintf("%s greeter launch-session --from-memory --cache-dir %s", stableDMSCommand(), shellQuote(cacheDir)) - escaped := strings.ReplaceAll(launcher, `'`, `'\''`) - inner := fmt.Sprintf("env XDG_SESSION_TYPE=wayland sh -c 'exec %s'", escaped) - tomlEscaped := strings.ReplaceAll(inner, `\`, `\\`) - tomlEscaped = strings.ReplaceAll(tomlEscaped, `"`, `\"`) - return fmt.Sprintf(`command = "%s"`, tomlEscaped) -} - -func upsertInitialSession(configContent, loginUser, cacheDir string, enabled bool) string { - if !enabled { - return removeTomlSection(configContent, "initial_session") - } - - commandLine := formatInitialSessionCommand(cacheDir) - lines := strings.Split(configContent, "\n") - var out []string - - inInitialSession := false - foundInitialSession := false - initialSessionUserSet := false - initialSessionCommandSet := false - - appendInitialSessionFields := func() { - if !initialSessionUserSet { - out = append(out, fmt.Sprintf(`user = "%s"`, loginUser)) - } - if !initialSessionCommandSet { - out = append(out, commandLine) - } - } - - for _, line := range lines { - if section, ok := parseTomlSection(line); ok { - if inInitialSession { - appendInitialSessionFields() - } - - inInitialSession = section == "initial_session" - if inInitialSession { - foundInitialSession = true - initialSessionUserSet = false - initialSessionCommandSet = false - } - - out = append(out, line) - continue - } - - if inInitialSession { - trimmed := stripTomlComment(line) - if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") { - out = append(out, fmt.Sprintf(`user = "%s"`, loginUser)) - initialSessionUserSet = true - continue - } - - if strings.HasPrefix(trimmed, "command =") || strings.HasPrefix(trimmed, "command=") { - if !initialSessionCommandSet { - out = append(out, commandLine) - initialSessionCommandSet = true - } - continue - } - } - - out = append(out, line) - } - - if inInitialSession { - appendInitialSessionFields() - } - - if !foundInitialSession { - if len(out) > 0 && strings.TrimSpace(out[len(out)-1]) != "" { - out = append(out, "") - } - out = append(out, "[initial_session]") - out = append(out, fmt.Sprintf(`user = "%s"`, loginUser)) - out = append(out, commandLine) - } - - return strings.Join(out, "\n") -} - -type greeterAutoLoginConfig struct { - GreeterAutoLogin bool `json:"greeterAutoLogin"` - GreeterRememberLastUser bool `json:"greeterRememberLastUser"` - GreeterRememberLastSession bool `json:"greeterRememberLastSession"` -} - -type greeterAutoLoginMemory struct { - LastSuccessfulUser string `json:"lastSuccessfulUser"` - LastSessionID string `json:"lastSessionId"` - LastSessionDesktopID string `json:"lastSessionDesktopId"` - LastSessionExec string `json:"lastSessionExec"` - AutoLoginEnabled bool `json:"autoLoginEnabled"` -} - -func readGreeterAutoLoginConfig(settingsPath string) (greeterAutoLoginConfig, error) { - cfg := greeterAutoLoginConfig{ - GreeterRememberLastUser: true, - GreeterRememberLastSession: true, - } - data, err := os.ReadFile(settingsPath) - if err != nil { - if os.IsNotExist(err) { - return cfg, nil - } - return cfg, err - } - if err := json.Unmarshal(data, &cfg); err != nil { - return cfg, fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err) - } - return cfg, nil -} - -func readGreeterAutoLoginMemory(memoryPath string) (greeterAutoLoginMemory, error) { - var mem greeterAutoLoginMemory - data, err := os.ReadFile(memoryPath) - if err != nil { - if os.IsNotExist(err) { - return mem, nil - } - return mem, err - } - if err := json.Unmarshal(data, &mem); err != nil { - return mem, fmt.Errorf("failed to parse greeter memory at %s: %w", memoryPath, err) - } - return mem, nil -} - -func execFromDesktopFile(path string) (string, error) { - data, err := os.ReadFile(path) - if err != nil { - return "", err - } - inDesktopEntry := false - for line := range strings.SplitSeq(string(data), "\n") { - trimmed := strings.TrimSpace(line) - switch { - case trimmed == "" || strings.HasPrefix(trimmed, "#"): - continue - case strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]"): - inDesktopEntry = trimmed == "[Desktop Entry]" - continue - case !inDesktopEntry: - continue - } - key, value, found := strings.Cut(trimmed, "=") - if !found || strings.TrimSpace(key) != "Exec" { - continue - } - return strings.TrimSpace(value), nil - } - return "", fmt.Errorf("no Exec= line found in %s", path) -} - -func resolveGreeterAutoLoginState(cacheDir, homeDir string) (enabled bool, loginUser string, sessionID string, err error) { - settingsPath := filepath.Join(cacheDir, "settings.json") - if _, statErr := os.Stat(settingsPath); statErr != nil { - settingsPath = filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json") - } - - cfg, err := readGreeterAutoLoginConfig(settingsPath) - if err != nil { - return false, "", "", err - } - - memoryPath := filepath.Join(cacheDir, ".local/state/memory.json") - mem, err := readGreeterAutoLoginMemory(memoryPath) - if err != nil { - return false, "", "", err - } - - enabled = cfg.GreeterAutoLogin - if !enabled { - return false, "", "", nil - } - - if !cfg.GreeterRememberLastUser || !cfg.GreeterRememberLastSession { - return true, "", "", nil - } - - loginUser = mem.LastSuccessfulUser - if loginUser == "" { - current, userErr := user.Current() - if userErr != nil { - return true, "", "", userErr - } - loginUser = current.Username - } - - sessionID = sessionDesktopIDFromMemory(mem) - - return true, loginUser, sessionID, nil -} - -func writeGreetdConfig(configPath, content string, logFunc func(string), sudoPassword, successMsg string) error { - if err := backupFileIfExists(sudoPassword, configPath, ".backup"); err != nil { - return fmt.Errorf("failed to backup greetd config: %w", err) - } - - tmpFile, err := os.CreateTemp("", "greetd-config-*.toml") - if err != nil { - return fmt.Errorf("failed to create temp greetd config: %w", err) - } - defer os.Remove(tmpFile.Name()) - - if _, err := tmpFile.WriteString(content); err != nil { - _ = tmpFile.Close() - return fmt.Errorf("failed to write temp greetd config: %w", err) - } - if err := tmpFile.Close(); err != nil { - return fmt.Errorf("failed to close temp greetd config: %w", err) - } - - if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", "/etc/greetd"); err != nil { - return fmt.Errorf("failed to create /etc/greetd: %w", err) - } - - if err := privesc.Run(context.Background(), sudoPassword, "install", "-o", "root", "-g", "root", "-m", "0644", tmpFile.Name(), configPath); err != nil { - return fmt.Errorf("failed to install greetd config: %w", err) - } - - if logFunc != nil && successMsg != "" { - logFunc(successMsg) - } - return nil -} - -func clearGreeterAutoLoginMemory(memoryPath, sudoPassword string) error { - data, err := readGreeterMemoryFile(memoryPath, sudoPassword) - if err != nil { - if os.IsNotExist(err) { - return nil - } - return err - } - if len(strings.TrimSpace(string(data))) == 0 { - return nil - } - var raw map[string]json.RawMessage - if err := json.Unmarshal(data, &raw); err != nil { - return fmt.Errorf("failed to parse greeter memory at %s: %w", memoryPath, err) - } - if _, ok := raw["autoLoginEnabled"]; !ok { - return nil - } - delete(raw, "autoLoginEnabled") - encoded, err := json.MarshalIndent(raw, "", " ") - if err != nil { - return err - } - if len(encoded) == 0 || string(encoded) == "null" { - encoded = []byte("{}") - } - encoded = append(encoded, '\n') - - if err := os.WriteFile(memoryPath, encoded, 0o644); err == nil { - return nil - } else if !os.IsPermission(err) { - return err - } - - tmpFile, err := os.CreateTemp("", "greeter-memory-*.json") - if err != nil { - return fmt.Errorf("failed to create temp greeter memory file: %w", err) - } - defer os.Remove(tmpFile.Name()) - - if _, err := tmpFile.Write(encoded); err != nil { - _ = tmpFile.Close() - return fmt.Errorf("failed to write temp greeter memory file: %w", err) - } - if err := tmpFile.Close(); err != nil { - return fmt.Errorf("failed to close temp greeter memory file: %w", err) - } - - greeterUser := DetectGreeterUser() - greeterGroup := DetectGreeterGroup() - owner := greeterUser + ":" + greeterGroup - if err := privesc.Run(context.Background(), sudoPassword, "install", "-o", greeterUser, "-g", greeterGroup, "-m", "0664", tmpFile.Name(), memoryPath); err != nil { - if fallbackErr := privesc.Run(context.Background(), sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0664", tmpFile.Name(), memoryPath); fallbackErr != nil { - return fmt.Errorf("failed to install greeter memory file (preferred %s: %w; fallback root:%s: %v)", owner, err, greeterGroup, fallbackErr) - } - } - return nil -} - -func readGreeterMemoryFile(memoryPath, sudoPassword string) ([]byte, error) { - data, err := os.ReadFile(memoryPath) - if err == nil || !os.IsPermission(err) { - return data, err - } - - tmpFile, err := os.CreateTemp("", "greeter-memory-read-*") - if err != nil { - return nil, fmt.Errorf("failed to create temp file for greeter memory read: %w", err) - } - tmpPath := tmpFile.Name() - _ = tmpFile.Close() - defer os.Remove(tmpPath) - - if err := privesc.Run(context.Background(), sudoPassword, "cp", "-f", memoryPath, tmpPath); err != nil { - return nil, fmt.Errorf("failed to read greeter memory at %s: %w", memoryPath, err) - } - return os.ReadFile(tmpPath) -} - -func SyncGreetdAutoLogin(cacheDir, homeDir string, logFunc func(string), sudoPassword string) error { - enabled, loginUser, sessionID, err := resolveGreeterAutoLoginState(cacheDir, homeDir) - if err != nil { - return err - } - - configPath := "/etc/greetd/config.toml" - configContent := "" - if data, readErr := os.ReadFile(configPath); readErr == nil { - configContent = string(data) - } else if !os.IsNotExist(readErr) { - return fmt.Errorf("failed to read greetd config: %w", readErr) - } - - if !enabled { - memoryPath := filepath.Join(cacheDir, ".local/state/memory.json") - if err := clearGreeterAutoLoginMemory(memoryPath, sudoPassword); err != nil && logFunc != nil { - logFunc(fmt.Sprintf("⚠ Warning: Failed to clear greeter auto-login memory flag: %v", err)) - } - newConfig := upsertInitialSession(configContent, "", "", false) - if newConfig == configContent { - if logFunc != nil { - logFunc("βœ“ Greeter auto-login disabled") - } - return nil - } - return writeGreetdConfig(configPath, newConfig, logFunc, sudoPassword, "βœ“ Disabled greeter auto-login") - } - - if loginUser == "" || sessionID == "" { - if logFunc != nil { - logFunc("⚠ Greeter auto-login is enabled but user or session is not configured yet. Log in manually once, then run sync.") - } - newConfig := upsertInitialSession(configContent, "", "", false) - if newConfig != configContent { - return writeGreetdConfig(configPath, newConfig, nil, sudoPassword, "") - } - return nil - } - - newConfig := upsertInitialSession(configContent, loginUser, cacheDir, true) - if newConfig == configContent { - if logFunc != nil { - logFunc(fmt.Sprintf("βœ“ Greeter auto-login already configured for %s", loginUser)) - } - memoryPath := filepath.Join(cacheDir, ".local/state/memory.json") - _ = clearGreeterAutoLoginMemory(memoryPath, sudoPassword) - return nil - } - - if err := writeGreetdConfig(configPath, newConfig, logFunc, sudoPassword, fmt.Sprintf("βœ“ Configured greeter auto-login for %s", loginUser)); err != nil { - return err - } - memoryPath := filepath.Join(cacheDir, ".local/state/memory.json") - if err := clearGreeterAutoLoginMemory(memoryPath, sudoPassword); err != nil && logFunc != nil { - logFunc(fmt.Sprintf("⚠ Warning: Failed to clear greeter auto-login memory flag: %v", err)) - } - return nil -} - -func SyncGreeterAutoLoginOnly(logFunc func(string), sudoPassword string) error { - homeDir, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("failed to get user home directory: %w", err) - } - return SyncGreetdAutoLogin(GreeterCacheDir, homeDir, logFunc, sudoPassword) -} - -func DetectGreeterUser() string { - passwdData, err := os.ReadFile("/etc/passwd") - if err == nil { - passwdContent := string(passwdData) - - if configData, cfgErr := os.ReadFile("/etc/greetd/config.toml"); cfgErr == nil { - if configured := extractDefaultSessionUser(string(configData)); configured != "" && hasPasswdUser(passwdContent, configured) { - return configured - } - } - - if user, found := findPasswdUser(passwdContent, "greeter", "greetd", "_greeter"); found { - return user - } - } else { - fmt.Fprintln(os.Stderr, "⚠ Warning: could not read /etc/passwd, defaulting greeter user to 'greeter'") - } - - if configData, cfgErr := os.ReadFile("/etc/greetd/config.toml"); cfgErr == nil { - if configured := extractDefaultSessionUser(string(configData)); configured != "" { - return configured - } - } - - fmt.Fprintln(os.Stderr, "⚠ Warning: no greeter user found, defaulting to 'greeter'") - return "greeter" -} - -func resolveGreeterWrapperPath() string { - if override := strings.TrimSpace(os.Getenv("DMS_GREETER_WRAPPER_CMD")); override != "" { - return override - } - - // Packaged installs only use the official wrapper; never fall back to /usr/local/bin. - if IsGreeterPackaged() { - packagedWrapper := "/usr/bin/dms-greeter" - if info, err := os.Stat(packagedWrapper); err == nil && !info.IsDir() && (info.Mode()&0o111) != 0 { - return packagedWrapper - } - fmt.Fprintln(os.Stderr, "⚠ Warning: packaged dms-greeter detected, but /usr/bin/dms-greeter is missing or not executable") - return packagedWrapper - } - - for _, candidate := range []string{"/usr/bin/dms-greeter", "/usr/local/bin/dms-greeter"} { - if info, err := os.Stat(candidate); err == nil && !info.IsDir() && (info.Mode()&0o111) != 0 { - return candidate - } - } - - if path, err := exec.LookPath("dms-greeter"); err == nil { - resolved := path - if realPath, realErr := filepath.EvalSymlinks(path); realErr == nil { - resolved = realPath - } - if strings.HasPrefix(resolved, "/home/") || strings.HasPrefix(resolved, "/tmp/") { - fmt.Fprintf(os.Stderr, "⚠ Warning: ignoring non-system dms-greeter on PATH: %s\n", path) - } else { - return path - } - } - - return "/usr/bin/dms-greeter" -} - -func DetectCompositors() []string { - var compositors []string - - if utils.CommandExists("niri") { - compositors = append(compositors, "niri") - } - if utils.CommandExists("Hyprland") { - compositors = append(compositors, "Hyprland") - } - if utils.CommandExists("mango") { - compositors = append(compositors, "mango") - } - - return compositors -} - -func PromptCompositorChoice(compositors []string) (string, error) { - fmt.Println("\nMultiple compositors detected:") - for i, comp := range compositors { - fmt.Printf("%d) %s\n", i+1, comp) - } - - reader := bufio.NewReader(os.Stdin) - fmt.Print("Choose compositor for greeter (1-2): ") - response, err := reader.ReadString('\n') - if err != nil { - return "", fmt.Errorf("error reading input: %w", err) - } - - response = strings.TrimSpace(response) - switch response { - case "1": - return compositors[0], nil - case "2": - if len(compositors) > 1 { - return compositors[1], nil - } - return "", fmt.Errorf("invalid choice") - default: - return "", fmt.Errorf("invalid choice") - } -} - -// EnsureGreetdInstalled checks if greetd is installed - greetd is a daemon in /usr/sbin on Debian/Ubuntu -func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error { - greetdFound := utils.CommandExists("greetd") - if !greetdFound { - for _, p := range []string{"/usr/sbin/greetd", "/sbin/greetd"} { - if _, err := os.Stat(p); err == nil { - greetdFound = true - break - } - } - } - if greetdFound { - logFunc("βœ“ greetd is already installed") - return nil - } - - logFunc("greetd is not installed. Installing...") - - osInfo, err := distros.GetOSInfo() - if err != nil { - return fmt.Errorf("failed to detect OS: %w", err) - } - - config, exists := distros.Registry[osInfo.Distribution.ID] - if !exists { - return fmt.Errorf("unsupported distribution for automatic greetd installation: %s", osInfo.Distribution.ID) - } - - ctx := context.Background() - var installCmd *exec.Cmd - - switch config.Family { - case distros.FamilyArch: - installCmd = privesc.ExecCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm greetd") - case distros.FamilyFedora: - installCmd = privesc.ExecCommand(ctx, sudoPassword, "dnf install -y greetd") - case distros.FamilySUSE: - installCmd = privesc.ExecCommand(ctx, sudoPassword, "zypper install -y greetd") - case distros.FamilyUbuntu, distros.FamilyDebian: - installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y greetd") - case distros.FamilyGentoo: - installCmd = privesc.ExecCommand(ctx, sudoPassword, "emerge --ask n sys-apps/greetd") - case distros.FamilyVoid: - installCmd = privesc.ExecCommand(ctx, sudoPassword, "xbps-install -Sy greetd") - case distros.FamilyNix: - return fmt.Errorf("on NixOS, please add greetd to your configuration.nix") - default: - return fmt.Errorf("unsupported distribution family for automatic greetd installation: %s", config.Family) - } - - installCmd.Stdout = os.Stdout - installCmd.Stderr = os.Stderr - - if err := installCmd.Run(); err != nil { - return fmt.Errorf("failed to install greetd: %w", err) - } - - logFunc("βœ“ greetd installed successfully") - return nil -} - -// IsGreeterPackaged returns true if dms-greeter was installed from a system package. -func IsGreeterPackaged() bool { - if !utils.CommandExists("dms-greeter") { - return false - } - packagedPath := "/usr/share/quickshell/dms-greeter" - info, err := os.Stat(packagedPath) - return err == nil && info.IsDir() -} - -// HasLegacyLocalGreeterWrapper returns true when a manually installed wrapper exists. -func HasLegacyLocalGreeterWrapper() bool { - info, err := os.Stat("/usr/local/bin/dms-greeter") - return err == nil && !info.IsDir() -} - -// TryInstallGreeterPackage attempts to install dms-greeter from the distro's official repo. -func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool { - osInfo, err := distros.GetOSInfo() - if err != nil { - return false - } - config, exists := distros.Registry[osInfo.Distribution.ID] - if !exists { - return false - } - - if IsGreeterPackaged() { - logFunc("βœ“ dms-greeter package already installed") - return true - } - - ctx := context.Background() - var installCmd *exec.Cmd - var failHint string - - switch config.Family { - case distros.FamilyDebian: - obsSlug := getDebianOBSSlug(osInfo) - keyURL := fmt.Sprintf("https://download.opensuse.org/repositories/home:AvengeMedia:danklinux/%s/Release.key", obsSlug) - repoLine := fmt.Sprintf("deb [signed-by=/etc/apt/keyrings/danklinux.gpg] https://download.opensuse.org/repositories/home:/AvengeMedia:/danklinux/%s/ /", obsSlug) - failHint = fmt.Sprintf("⚠ dms-greeter install failed. Add OBS repo manually:\nsudo apt-get install -y gnupg\nsudo mkdir -p /etc/apt/keyrings\ncurl -fsSL %s | sudo gpg --dearmor -o /etc/apt/keyrings/danklinux.gpg\necho '%s' | sudo tee /etc/apt/sources.list.d/danklinux.list\nsudo apt update && sudo apt-get install -y dms-greeter", keyURL, repoLine) - logFunc(fmt.Sprintf("Adding DankLinux OBS repository (%s)...", obsSlug)) - if _, err := exec.LookPath("gpg"); err != nil { - logFunc("Installing gnupg for OBS repository key import...") - installGPGCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y gnupg") - installGPGCmd.Stdout = os.Stdout - installGPGCmd.Stderr = os.Stderr - if err := installGPGCmd.Run(); err != nil { - logFunc(fmt.Sprintf("⚠ Failed to install gnupg: %v", err)) - } - } - mkdirCmd := privesc.ExecCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings") - mkdirCmd.Stdout = os.Stdout - mkdirCmd.Stderr = os.Stderr - mkdirCmd.Run() - addKeyCmd := privesc.ExecCommand(ctx, sudoPassword, - fmt.Sprintf(`bash -c "curl -fsSL %s | gpg --dearmor -o /etc/apt/keyrings/danklinux.gpg"`, keyURL)) - addKeyCmd.Stdout = os.Stdout - addKeyCmd.Stderr = os.Stderr - addKeyCmd.Run() - addRepoCmd := privesc.ExecCommand(ctx, sudoPassword, - fmt.Sprintf(`bash -c "echo '%s' > /etc/apt/sources.list.d/danklinux.list"`, repoLine)) - addRepoCmd.Stdout = os.Stdout - addRepoCmd.Stderr = os.Stderr - addRepoCmd.Run() - privesc.ExecCommand(ctx, sudoPassword, "apt-get update").Run() - installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y dms-greeter") - case distros.FamilySUSE: - repoURL := getOpenSUSEOBSRepoURL(osInfo) - failHint = fmt.Sprintf("⚠ dms-greeter install failed. Add OBS repo manually:\nsudo zypper addrepo %s\nsudo zypper refresh && sudo zypper install dms-greeter", repoURL) - logFunc("Adding DankLinux OBS repository...") - addRepoCmd := privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("zypper addrepo %s", repoURL)) - addRepoCmd.Stdout = os.Stdout - addRepoCmd.Stderr = os.Stderr - addRepoCmd.Run() - privesc.ExecCommand(ctx, sudoPassword, "zypper refresh").Run() - installCmd = privesc.ExecCommand(ctx, sudoPassword, "zypper install -y dms-greeter") - case distros.FamilyUbuntu: - failHint = "⚠ dms-greeter install failed. Add PPA manually: sudo add-apt-repository ppa:avengemedia/danklinux && sudo apt-get update && sudo apt-get install -y dms-greeter" - logFunc("Enabling PPA ppa:avengemedia/danklinux...") - ppacmd := privesc.ExecCommand(ctx, sudoPassword, "add-apt-repository -y ppa:avengemedia/danklinux") - ppacmd.Stdout = os.Stdout - ppacmd.Stderr = os.Stderr - ppacmd.Run() - privesc.ExecCommand(ctx, sudoPassword, "apt-get update").Run() - installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y dms-greeter") - case distros.FamilyFedora: - failHint = "⚠ dms-greeter install failed. Enable COPR manually: sudo dnf copr enable avengemedia/danklinux && sudo dnf install dms-greeter" - logFunc("Enabling COPR avengemedia/danklinux...") - coprcmd := privesc.ExecCommand(ctx, sudoPassword, "dnf copr enable -y avengemedia/danklinux") - coprcmd.Stdout = os.Stdout - coprcmd.Stderr = os.Stderr - coprcmd.Run() - installCmd = privesc.ExecCommand(ctx, sudoPassword, "dnf install -y dms-greeter") - case distros.FamilyArch: - aurHelper := "" - for _, helper := range []string{"paru", "yay"} { - if _, err := exec.LookPath(helper); err == nil { - aurHelper = helper - break - } - } - if aurHelper == "" { - logFunc("⚠ No AUR helper found (paru/yay). Install greetd-dms-greeter-git from AUR: https://aur.archlinux.org/packages/greetd-dms-greeter-git") - return false - } - failHint = fmt.Sprintf("⚠ dms-greeter install failed. Install from AUR: %s -S greetd-dms-greeter-git", aurHelper) - installCmd = exec.CommandContext(ctx, aurHelper, "-S", "--noconfirm", "greetd-dms-greeter-git") - case distros.FamilyVoid: - failHint = "⚠ dms-greeter install failed. Add the DMS XBPS repo manually:\necho 'repository=https://void.danklinux.com/dms/current' | sudo tee /etc/xbps.d/dms.conf\nsudo xbps-install -Sy dms-greeter" - logFunc("Adding DMS XBPS repository...") - if err := ensureVoidXBPSRepo(ctx, sudoPassword, "dms", distros.VoidDMSRepo); err != nil { - logFunc(fmt.Sprintf("⚠ Failed to add DMS XBPS repository: %v", err)) - } - privesc.ExecCommand(ctx, sudoPassword, "sh -c 'yes y | xbps-install -Sy -i --repository "+distros.VoidDMSRepo+"'").Run() - installCmd = privesc.ExecCommand(ctx, sudoPassword, "xbps-install -Sy dms-greeter") - default: - return false - } - - logFunc("Installing dms-greeter from official repository...") - installCmd.Stdout = os.Stdout - installCmd.Stderr = os.Stderr - - if err := installCmd.Run(); err != nil { - logFunc(failHint) - return false - } - - logFunc("βœ“ dms-greeter package installed") - return true -} - -func ensureVoidXBPSRepo(ctx context.Context, sudoPassword, name, repoURL string) error { - confPath := filepath.Join("/etc/xbps.d", name+".conf") - repoLine := fmt.Sprintf("repository=%s", repoURL) - repoFileContent := repoLine + "\n" - if content, err := os.ReadFile(confPath); err == nil && string(content) == repoFileContent { - return nil - } - if err := privesc.Run(ctx, sudoPassword, "mkdir", "-p", "/etc/xbps.d"); err != nil { - return err - } - return privesc.Run(ctx, sudoPassword, "sh", "-c", - fmt.Sprintf("printf '%%s\\n' %q > %s", repoLine, confPath)) -} - -// CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory -func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error { - if IsGreeterPackaged() { - logFunc("βœ“ dms-greeter package already installed") - } else { - if dmsPath == "" { - return fmt.Errorf("dms path is required for manual dms-greeter wrapper installs") - } - - assetsDir := filepath.Join(dmsPath, "Modules", "Greetd", "assets") - wrapperSrc := filepath.Join(assetsDir, "dms-greeter") - - if _, err := os.Stat(wrapperSrc); os.IsNotExist(err) { - return fmt.Errorf("dms-greeter wrapper not found at %s", wrapperSrc) - } - - wrapperDst := "/usr/local/bin/dms-greeter" - action := "Installed" - if info, err := os.Stat(wrapperDst); err == nil && !info.IsDir() { - action = "Updated" - } - if err := privesc.Run(context.Background(), sudoPassword, "cp", wrapperSrc, wrapperDst); err != nil { - return fmt.Errorf("failed to copy dms-greeter wrapper: %w", err) - } - logFunc(fmt.Sprintf("βœ“ %s dms-greeter wrapper at %s", action, wrapperDst)) - - if err := privesc.Run(context.Background(), sudoPassword, "chmod", "+x", wrapperDst); err != nil { - return fmt.Errorf("failed to make wrapper executable: %w", err) - } - - osInfo, err := distros.GetOSInfo() - if err == nil { - if config, exists := distros.Registry[osInfo.Distribution.ID]; exists && (config.Family == distros.FamilyFedora || config.Family == distros.FamilySUSE) { - if err := privesc.Run(context.Background(), sudoPassword, "semanage", "fcontext", "-a", "-t", "bin_t", wrapperDst); err != nil { - logFunc(fmt.Sprintf("⚠ Warning: Failed to set SELinux fcontext: %v", err)) - } else { - logFunc("βœ“ Set SELinux fcontext for dms-greeter") - } - - if err := privesc.Run(context.Background(), sudoPassword, "restorecon", "-v", wrapperDst); err != nil { - logFunc(fmt.Sprintf("⚠ Warning: Failed to restore SELinux context: %v", err)) - } else { - logFunc("βœ“ Restored SELinux context for dms-greeter") - } - } - } - } - - if err := EnsureGreeterCacheDir(logFunc, sudoPassword); err != nil { - return err - } - - return nil -} - -// EnsureGreeterCacheDir creates /var/cache/dms-greeter with correct ownership if it does not exist. -// It is safe to call multiple times (idempotent) and will repair ownership/mode -// when the directory already exists with stale permissions. -func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error { - cacheDir := GreeterCacheDir - created := false - if info, err := os.Stat(cacheDir); err != nil { - if !os.IsNotExist(err) { - return fmt.Errorf("failed to stat cache directory: %w", err) - } - if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", cacheDir); err != nil { - return fmt.Errorf("failed to create cache directory: %w", err) - } - created = true - } else if !info.IsDir() { - return fmt.Errorf("cache path exists but is not a directory: %s", cacheDir) - } - - group := DetectGreeterGroup() - daemonUser := DetectGreeterUser() - preferredOwner := fmt.Sprintf("%s:%s", daemonUser, group) - owner := preferredOwner - if err := privesc.Run(context.Background(), sudoPassword, "chown", owner, cacheDir); err != nil { - // Some setups may not have a matching daemon user at this moment; fall back - // to root: while still allowing group-writable greeter runtime access. - fallbackOwner := fmt.Sprintf("root:%s", group) - if fallbackErr := privesc.Run(context.Background(), sudoPassword, "chown", fallbackOwner, cacheDir); fallbackErr != nil { - return fmt.Errorf("failed to set cache directory owner (preferred %s: %v; fallback %s: %w)", preferredOwner, err, fallbackOwner, fallbackErr) - } - owner = fallbackOwner - } - - if err := privesc.Run(context.Background(), sudoPassword, "chmod", "2770", cacheDir); err != nil { - return fmt.Errorf("failed to set cache directory permissions: %w", err) - } - - runtimeDirs := []string{ - filepath.Join(cacheDir, "users"), - filepath.Join(cacheDir, ".local"), - filepath.Join(cacheDir, ".local", "state"), - filepath.Join(cacheDir, ".local", "share"), - filepath.Join(cacheDir, ".cache"), - } - for _, dir := range runtimeDirs { - if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", dir); err != nil { - return fmt.Errorf("failed to create cache runtime directory %s: %w", dir, err) - } - if err := privesc.Run(context.Background(), sudoPassword, "chown", owner, dir); err != nil { - return fmt.Errorf("failed to set owner for cache runtime directory %s: %w", dir, err) - } - if err := privesc.Run(context.Background(), sudoPassword, "chmod", "2770", dir); err != nil { - return fmt.Errorf("failed to set permissions for cache runtime directory %s: %w", dir, err) - } - } - - legacyMemoryPath := filepath.Join(cacheDir, "memory.json") - stateMemoryPath := filepath.Join(cacheDir, ".local", "state", "memory.json") - if err := ensureGreeterMemoryCompatLink(logFunc, sudoPassword, legacyMemoryPath, stateMemoryPath); err != nil { - return err - } - - if isSELinuxEnforcing() && utils.CommandExists("restorecon") { - if err := privesc.Run(context.Background(), sudoPassword, "restorecon", "-Rv", cacheDir); err != nil { - logFunc(fmt.Sprintf("⚠ Warning: Failed to restore SELinux context for %s: %v", cacheDir, err)) - } - } - - if created { - logFunc(fmt.Sprintf("βœ“ Created cache directory %s (owner: %s, mode: 2770)", cacheDir, owner)) - } else { - logFunc(fmt.Sprintf("βœ“ Ensured cache directory %s permissions (owner: %s, mode: 2770)", cacheDir, owner)) - } - return nil -} - -func isSELinuxEnforcing() bool { - data, err := os.ReadFile("/sys/fs/selinux/enforce") - if err != nil { - return false - } - return strings.TrimSpace(string(data)) == "1" -} - -func ensureGreeterMemoryCompatLink(logFunc func(string), sudoPassword, legacyPath, statePath string) error { - info, err := os.Lstat(legacyPath) - if err == nil && info.Mode().IsRegular() { - if _, stateErr := os.Stat(statePath); os.IsNotExist(stateErr) { - if copyErr := privesc.Run(context.Background(), sudoPassword, "cp", "-f", legacyPath, statePath); copyErr != nil { - logFunc(fmt.Sprintf("⚠ Warning: Failed to migrate legacy greeter memory file to %s: %v", statePath, copyErr)) - } - } - } - - if err := privesc.Run(context.Background(), sudoPassword, "ln", "-sfn", statePath, legacyPath); err != nil { - return fmt.Errorf("failed to create greeter memory compatibility symlink %s -> %s: %w", legacyPath, statePath, err) - } - - return nil -} - -// IsAppArmorEnabled reports whether AppArmor is active on the running kernel. -func IsAppArmorEnabled() bool { - data, err := os.ReadFile("/sys/module/apparmor/parameters/enabled") - if err != nil { - return false - } - return strings.HasPrefix(strings.TrimSpace(strings.ToLower(string(data))), "y") -} - -// InstallAppArmorProfile installs the bundled AppArmor profile and reloads it. No-op on NixOS or non-AppArmor systems. -func InstallAppArmorProfile(logFunc func(string), sudoPassword string) error { - if IsNixOS() { - logFunc(" β„Ή Skipping AppArmor profile on NixOS (manage via security.apparmor.policies)") - return nil - } - - if !IsAppArmorEnabled() { - return nil - } - - if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", "/etc/apparmor.d"); err != nil { - return fmt.Errorf("failed to create /etc/apparmor.d: %w", err) - } - - tmp, err := os.CreateTemp("", "dms-apparmor-*") - if err != nil { - return fmt.Errorf("failed to create temp file for AppArmor profile: %w", err) - } - tmpPath := tmp.Name() - defer os.Remove(tmpPath) - - if _, err := tmp.Write(appArmorProfileData); err != nil { - tmp.Close() - return fmt.Errorf("failed to write AppArmor profile: %w", err) - } - tmp.Close() - - if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, appArmorProfileDest); err != nil { - return fmt.Errorf("failed to install AppArmor profile to %s: %w", appArmorProfileDest, err) - } - if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", appArmorProfileDest); err != nil { - return fmt.Errorf("failed to set AppArmor profile permissions: %w", err) - } - - if utils.CommandExists("apparmor_parser") { - if err := privesc.Run(context.Background(), sudoPassword, "apparmor_parser", "-r", appArmorProfileDest); err != nil { - logFunc(fmt.Sprintf(" ⚠ AppArmor profile installed but reload failed: %v", err)) - logFunc(" Run: sudo apparmor_parser -r " + appArmorProfileDest) - } else { - logFunc(" βœ“ AppArmor profile installed and loaded (complain mode)") - } - } else { - logFunc(" βœ“ AppArmor profile installed at " + appArmorProfileDest) - logFunc(" apparmor_parser not found β€” profile will be loaded on next boot") - } - - return nil -} - -// UninstallAppArmorProfile removes the DMS AppArmor profile and reloads AppArmor. -// It is a no-op when AppArmor is not active or the profile does not exist. -func UninstallAppArmorProfile(logFunc func(string), sudoPassword string) error { - if IsNixOS() { - return nil - } - if _, err := os.Stat("/sys/module/apparmor"); os.IsNotExist(err) { - return nil - } - if _, err := os.Stat(appArmorProfileDest); os.IsNotExist(err) { - return nil - } - - if utils.CommandExists("apparmor_parser") { - _ = privesc.Run(context.Background(), sudoPassword, "apparmor_parser", "--remove", appArmorProfileDest) - } - if err := privesc.Run(context.Background(), sudoPassword, "rm", "-f", appArmorProfileDest); err != nil { - return fmt.Errorf("failed to remove AppArmor profile: %w", err) - } - logFunc(" βœ“ Removed DMS AppArmor profile") - return nil -} - -// EnsureACLInstalled installs the acl package (setfacl/getfacl) if not already present -func EnsureACLInstalled(logFunc func(string), sudoPassword string) error { - if utils.CommandExists("setfacl") { - return nil - } - - logFunc("setfacl not found – installing acl package...") - - osInfo, err := distros.GetOSInfo() - if err != nil { - return fmt.Errorf("failed to detect OS: %w", err) - } - - config, exists := distros.Registry[osInfo.Distribution.ID] - if !exists { - return fmt.Errorf("unsupported distribution for automatic acl installation: %s", osInfo.Distribution.ID) - } - - ctx := context.Background() - var installCmd *exec.Cmd - - switch config.Family { - case distros.FamilyArch: - installCmd = privesc.ExecCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm acl") - case distros.FamilyFedora: - installCmd = privesc.ExecCommand(ctx, sudoPassword, "dnf install -y acl") - case distros.FamilySUSE: - installCmd = privesc.ExecCommand(ctx, sudoPassword, "zypper install -y acl") - case distros.FamilyUbuntu, distros.FamilyDebian: - installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y acl") - case distros.FamilyGentoo: - installCmd = privesc.ExecCommand(ctx, sudoPassword, "emerge --ask n sys-fs/acl") - case distros.FamilyNix: - return fmt.Errorf("on NixOS, please add pkgs.acl to your configuration.nix") - default: - return fmt.Errorf("unsupported distribution family for automatic acl installation: %s", config.Family) - } - - installCmd.Stdout = os.Stdout - installCmd.Stderr = os.Stderr - if err := installCmd.Run(); err != nil { - return fmt.Errorf("failed to install acl: %w", err) - } - - logFunc("βœ“ acl package installed") - return nil -} - -// SetupParentDirectoryACLs sets ACLs on parent directories to allow traversal -func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error { - if err := EnsureACLInstalled(logFunc, sudoPassword); err != nil { - logFunc(fmt.Sprintf("⚠ Warning: could not install acl package: %v", err)) - logFunc(" ACL permissions will be skipped; theme sync may not work correctly.") - return nil - } - if !utils.CommandExists("setfacl") { - logFunc("⚠ Warning: setfacl still not available after install attempt; skipping ACL setup.") - return nil - } - - homeDir, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("failed to get user home directory: %w", err) - } - - parentDirs := []struct { - path string - desc string - }{ - {homeDir, "home directory"}, - {filepath.Join(homeDir, ".config"), ".config directory"}, - {filepath.Join(homeDir, ".local"), ".local directory"}, - {filepath.Join(homeDir, ".cache"), ".cache directory"}, - {filepath.Join(homeDir, ".local", "state"), ".local/state directory"}, - {filepath.Join(homeDir, ".local", "share"), ".local/share directory"}, - } - - group := DetectGreeterGroup() - - logFunc("\nSetting up parent directory ACLs for greeter user access...") - - for _, dir := range parentDirs { - if _, err := os.Stat(dir.path); os.IsNotExist(err) { - if err := os.MkdirAll(dir.path, 0o755); err != nil { - logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", dir.desc, err)) - continue - } - } - - // Group ACL covers daemon users regardless of username (e.g. greetd β‰  greeter on Fedora). - if err := privesc.Run(context.Background(), sudoPassword, "setfacl", "-m", fmt.Sprintf("g:%s:rX", group), dir.path); err != nil { - logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err)) - logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m g:%s:rX %s", group, dir.path)) - continue - } - - logFunc(fmt.Sprintf("βœ“ Set ACL on %s", dir.desc)) - } - - return nil -} - -// RemediateStaleACLs removes user-based ACLs left by older binary versions. Best-effort. -func RemediateStaleACLs(logFunc func(string), sudoPassword string) { - if !utils.CommandExists("setfacl") { - return - } - - homeDir, err := os.UserHomeDir() - if err != nil { - return - } - - passwdData, err := os.ReadFile("/etc/passwd") - if err != nil { - return - } - - dirs := []string{ - homeDir, - filepath.Join(homeDir, ".config"), - filepath.Join(homeDir, ".config", "DankMaterialShell"), - filepath.Join(homeDir, ".cache"), - filepath.Join(homeDir, ".cache", "DankMaterialShell"), - filepath.Join(homeDir, ".local"), - filepath.Join(homeDir, ".local", "state"), - filepath.Join(homeDir, ".local", "share"), - } - - passwdContent := string(passwdData) - staleUsers := []string{"greeter", "greetd", "_greeter"} - existingUsers := make([]string, 0, len(staleUsers)) - for _, user := range staleUsers { - if hasPasswdUser(passwdContent, user) { - existingUsers = append(existingUsers, user) - } - } - if len(existingUsers) == 0 { - return - } - - cleaned := false - for _, dir := range dirs { - if _, err := os.Stat(dir); err != nil { - continue - } - for _, user := range existingUsers { - _ = privesc.Run(context.Background(), sudoPassword, "setfacl", "-x", fmt.Sprintf("u:%s", user), dir) - cleaned = true - } - } - if cleaned { - logFunc("βœ“ Cleaned up stale user-based ACLs from previous versions") - } -} - -// RemediateStaleAppArmor removes any AppArmor profile installed by an older binary on -// systems where AppArmor is not active. -func RemediateStaleAppArmor(logFunc func(string), sudoPassword string) { - if IsAppArmorEnabled() { - return - } - if _, err := os.Stat(appArmorProfileDest); os.IsNotExist(err) { - return - } - logFunc("β„Ή Removing stale AppArmor profile (AppArmor is not active on this system)") - _ = UninstallAppArmorProfile(logFunc, sudoPassword) -} - -func SetupDMSGroup(logFunc func(string), sudoPassword string) error { - homeDir, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("failed to get user home directory: %w", err) - } - - currentUser := os.Getenv("USER") - if currentUser == "" { - currentUser = os.Getenv("LOGNAME") - } - if currentUser == "" { - return fmt.Errorf("failed to determine current user") - } - - group := DetectGreeterGroup() - - // Create the group if it doesn't exist yet (e.g. before greetd package is installed). - if !utils.HasGroup(group) { - if err := privesc.Run(context.Background(), sudoPassword, "groupadd", "-r", group); err != nil { - return fmt.Errorf("failed to create %s group: %w", group, err) - } - logFunc(fmt.Sprintf("βœ“ Created system group %s", group)) - } - - groupsCmd := exec.Command("groups", currentUser) - groupsOutput, err := groupsCmd.Output() - if err == nil && strings.Contains(string(groupsOutput), group) { - logFunc(fmt.Sprintf("βœ“ %s is already in %s group", currentUser, group)) - } else { - if err := privesc.Run(context.Background(), sudoPassword, "usermod", "-aG", group, currentUser); err != nil { - return fmt.Errorf("failed to add %s to %s group: %w", currentUser, group, err) - } - logFunc(fmt.Sprintf("βœ“ Added %s to %s group (logout/login required for changes to take effect)", currentUser, group)) - } - - // Also add the daemon user (e.g. greetd on Fedora) so group ACLs apply to the running process. - daemonUser := DetectGreeterUser() - if daemonUser != currentUser { - daemonGroupsCmd := exec.Command("groups", daemonUser) - daemonGroupsOutput, daemonGroupsErr := daemonGroupsCmd.Output() - if daemonGroupsErr == nil { - if strings.Contains(string(daemonGroupsOutput), group) { - logFunc(fmt.Sprintf("βœ“ Greeter daemon user %s is already in %s group", daemonUser, group)) - } else { - if err := privesc.Run(context.Background(), sudoPassword, "usermod", "-aG", group, daemonUser); err != nil { - logFunc(fmt.Sprintf("⚠ Warning: could not add %s to %s group: %v", daemonUser, group, err)) - } else { - logFunc(fmt.Sprintf("βœ“ Added greeter daemon user %s to %s group", daemonUser, group)) - } - } - } - } - - configDirs := []struct { - path string - desc string - }{ - {filepath.Join(homeDir, ".config", "DankMaterialShell"), "DankMaterialShell config"}, - {filepath.Join(homeDir, ".local", "state", "DankMaterialShell"), "DankMaterialShell state"}, - {filepath.Join(homeDir, ".cache", "DankMaterialShell"), "DankMaterialShell cache"}, - {filepath.Join(homeDir, ".cache", "quickshell"), "quickshell cache"}, - {filepath.Join(homeDir, ".config", "quickshell"), "quickshell config"}, - {filepath.Join(homeDir, ".local", "share", "wayland-sessions"), "wayland sessions"}, - {filepath.Join(homeDir, ".local", "share", "xsessions"), "xsessions"}, - } - - for _, dir := range configDirs { - if _, err := os.Stat(dir.path); os.IsNotExist(err) { - if err := os.MkdirAll(dir.path, 0o755); err != nil { - logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", dir.path, err)) - continue - } - } - - if err := privesc.Run(context.Background(), sudoPassword, "chgrp", "-R", group, dir.path); err != nil { - logFunc(fmt.Sprintf("⚠ Warning: Failed to set group for %s: %v", dir.desc, err)) - continue - } - - if err := privesc.Run(context.Background(), sudoPassword, "chmod", "-R", "g+rX", dir.path); err != nil { - logFunc(fmt.Sprintf("⚠ Warning: Failed to set permissions for %s: %v", dir.desc, err)) - continue - } - - logFunc(fmt.Sprintf("βœ“ Set group permissions for %s", dir.desc)) - } - - if err := SetupParentDirectoryACLs(logFunc, sudoPassword); err != nil { - return fmt.Errorf("failed to setup parent directory ACLs: %w", err) - } - - return nil -} - -type GreeterColorSyncInfo struct { - SourcePath string - ThemeName string - UsesDynamicWallpaperOverride bool -} - -type greeterThemeSyncSettings struct { - CurrentThemeName string `json:"currentThemeName"` - GreeterWallpaperPath string `json:"greeterWallpaperPath"` - MatugenScheme string `json:"matugenScheme"` - IconTheme string `json:"iconTheme"` -} - -type greeterThemeSyncSession struct { - IsLightMode bool `json:"isLightMode"` -} - -type greeterThemeSyncState struct { - ThemeName string - GreeterWallpaperPath string - ResolvedGreeterWallpaperPath string - MatugenScheme string - IconTheme string - IsLightMode bool - UsesDynamicWallpaperOverride bool -} - -func defaultGreeterColorsSource(homeDir string) string { - return filepath.Join(homeDir, ".cache", "DankMaterialShell", "dms-colors.json") -} - -func greeterOverrideColorsStateDir(homeDir string) string { - return filepath.Join(homeDir, ".cache", "DankMaterialShell", "greeter-colors") -} - -func greeterOverrideColorsSource(homeDir string) string { - return filepath.Join(greeterOverrideColorsStateDir(homeDir), "dms-colors.json") -} - -func readOptionalJSONFile(path string, dst any) error { - data, err := os.ReadFile(path) - if err != nil { - if os.IsNotExist(err) { - return nil - } - return err - } - if strings.TrimSpace(string(data)) == "" { - return nil - } - return json.Unmarshal(data, dst) -} - -func readGreeterThemeSyncSettings(homeDir string) (greeterThemeSyncSettings, error) { - settings := greeterThemeSyncSettings{ - CurrentThemeName: "purple", - MatugenScheme: "scheme-tonal-spot", - IconTheme: "System Default", - } - settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json") - if err := readOptionalJSONFile(settingsPath, &settings); err != nil { - return greeterThemeSyncSettings{}, fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err) - } - return settings, nil -} - -func readGreeterThemeSyncSession(homeDir string) (greeterThemeSyncSession, error) { - session := greeterThemeSyncSession{} - sessionPath := filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json") - if err := readOptionalJSONFile(sessionPath, &session); err != nil { - return greeterThemeSyncSession{}, fmt.Errorf("failed to parse session at %s: %w", sessionPath, err) - } - return session, nil -} - -func resolveGreeterThemeSyncState(homeDir string) (greeterThemeSyncState, error) { - settings, err := readGreeterThemeSyncSettings(homeDir) - if err != nil { - return greeterThemeSyncState{}, err - } - session, err := readGreeterThemeSyncSession(homeDir) - if err != nil { - return greeterThemeSyncState{}, err - } - - resolvedWallpaperPath := "" - if settings.GreeterWallpaperPath != "" { - resolvedWallpaperPath = settings.GreeterWallpaperPath - if !filepath.IsAbs(resolvedWallpaperPath) { - resolvedWallpaperPath = filepath.Join(homeDir, resolvedWallpaperPath) - } - } - - usesDynamicWallpaperOverride := strings.EqualFold(strings.TrimSpace(settings.CurrentThemeName), "dynamic") && resolvedWallpaperPath != "" - - return greeterThemeSyncState{ - ThemeName: settings.CurrentThemeName, - GreeterWallpaperPath: settings.GreeterWallpaperPath, - ResolvedGreeterWallpaperPath: resolvedWallpaperPath, - MatugenScheme: settings.MatugenScheme, - IconTheme: settings.IconTheme, - IsLightMode: session.IsLightMode, - UsesDynamicWallpaperOverride: usesDynamicWallpaperOverride, - }, nil -} - -func (s greeterThemeSyncState) effectiveColorsSource(homeDir string) string { - if s.UsesDynamicWallpaperOverride { - return greeterOverrideColorsSource(homeDir) - } - return defaultGreeterColorsSource(homeDir) -} - -func ResolveGreeterColorSyncInfo(homeDir string) (GreeterColorSyncInfo, error) { - state, err := resolveGreeterThemeSyncState(homeDir) - if err != nil { - return GreeterColorSyncInfo{}, err - } - return GreeterColorSyncInfo{ - SourcePath: state.effectiveColorsSource(homeDir), - ThemeName: state.ThemeName, - UsesDynamicWallpaperOverride: state.UsesDynamicWallpaperOverride, - }, nil -} - -func ensureGreeterSyncSourceFile(path string) error { - sourceDir := filepath.Dir(path) - if err := os.MkdirAll(sourceDir, 0o755); err != nil { - return fmt.Errorf("failed to create source directory %s: %w", sourceDir, err) - } - - if _, err := os.Stat(path); os.IsNotExist(err) { - if err := os.WriteFile(path, []byte("{}"), 0o644); err != nil { - return fmt.Errorf("failed to create source file %s: %w", path, err) - } - } else if err != nil { - return fmt.Errorf("failed to inspect source file %s: %w", path, err) - } - - return nil -} - -func syncGreeterDynamicOverrideColors(dmsPath, homeDir string, state greeterThemeSyncState, logFunc func(string)) error { - if !state.UsesDynamicWallpaperOverride { - return nil - } - - st, err := os.Stat(state.ResolvedGreeterWallpaperPath) - if err != nil { - return fmt.Errorf("configured greeter wallpaper not found at %s: %w", state.ResolvedGreeterWallpaperPath, err) - } - if st.IsDir() { - return fmt.Errorf("configured greeter wallpaper path points to a directory: %s", state.ResolvedGreeterWallpaperPath) - } - - mode := matugen.ColorModeDark - if state.IsLightMode { - mode = matugen.ColorModeLight - } - - opts := matugen.Options{ - StateDir: greeterOverrideColorsStateDir(homeDir), - ShellDir: dmsPath, - ConfigDir: filepath.Join(homeDir, ".config"), - Kind: "image", - Value: state.ResolvedGreeterWallpaperPath, - Mode: mode, - IconTheme: state.IconTheme, - MatugenType: state.MatugenScheme, - RunUserTemplates: false, - ColorsOnly: true, - } - - err = matugen.Run(opts) - switch { - case errors.Is(err, matugen.ErrNoChanges): - logFunc("βœ“ Greeter dynamic override colors already up to date") - return nil - case err != nil: - return fmt.Errorf("failed to generate greeter dynamic colors from wallpaper override: %w", err) - default: - logFunc("βœ“ Generated greeter dynamic colors from wallpaper override") - return nil - } -} - -func syncGreeterColorSource(homeDir, cacheDir string, state greeterThemeSyncState, logFunc func(string), sudoPassword string) error { - source := state.effectiveColorsSource(homeDir) - if !state.UsesDynamicWallpaperOverride { - if err := ensureGreeterSyncSourceFile(source); err != nil { - return err - } - } else if _, err := os.Stat(source); err != nil { - return fmt.Errorf("expected generated greeter colors at %s: %w", source, err) - } - - target := filepath.Join(cacheDir, "colors.json") - _ = privesc.Run(context.Background(), sudoPassword, "rm", "-f", target) - if err := privesc.Run(context.Background(), sudoPassword, "ln", "-sf", source, target); err != nil { - return fmt.Errorf("failed to create symlink for wallpaper based theming (%s -> %s): %w", target, source, err) - } - - if state.UsesDynamicWallpaperOverride { - logFunc("βœ“ Synced wallpaper based theming (greeter wallpaper override)") - } else { - logFunc("βœ“ Synced wallpaper based theming") - } - - return nil -} - -func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPassword string) error { - if err := EnsureVoidLogindGreetdCommand(logFunc, sudoPassword); err != nil { - return err - } - - homeDir, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("failed to get user home directory: %w", err) - } - - cacheDir := GreeterCacheDir - - symlinks := []struct { - source string - target string - desc string - }{ - { - source: filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"), - target: filepath.Join(cacheDir, "settings.json"), - desc: "core settings (theme, clock formats, etc)", - }, - { - source: filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"), - target: filepath.Join(cacheDir, "session.json"), - desc: "state (wallpaper configuration)", - }, - } - - for _, link := range symlinks { - sourceDir := filepath.Dir(link.source) - if _, err := os.Stat(sourceDir); os.IsNotExist(err) { - if err := os.MkdirAll(sourceDir, 0o755); err != nil { - return fmt.Errorf("failed to create source directory %s for %s: %w", sourceDir, link.desc, err) - } - } - - if _, err := os.Stat(link.source); os.IsNotExist(err) { - if err := os.WriteFile(link.source, []byte("{}"), 0o644); err != nil { - return fmt.Errorf("failed to create source file %s for %s: %w", link.source, link.desc, err) - } - } - - _ = privesc.Run(context.Background(), sudoPassword, "rm", "-f", link.target) - - if err := privesc.Run(context.Background(), sudoPassword, "ln", "-sf", link.source, link.target); err != nil { - return fmt.Errorf("failed to create symlink for %s (%s -> %s): %w", link.desc, link.target, link.source, err) - } - - logFunc(fmt.Sprintf("βœ“ Synced %s", link.desc)) - } - - state, err := resolveGreeterThemeSyncState(homeDir) - if err != nil { - return fmt.Errorf("failed to resolve greeter color source: %w", err) - } - - if err := syncGreeterDynamicOverrideColors(dmsPath, homeDir, state, logFunc); err != nil { - return err - } - - if err := syncGreeterColorSource(homeDir, cacheDir, state, logFunc, sudoPassword); err != nil { - return err - } - - if err := syncGreeterWallpaperOverride(cacheDir, logFunc, sudoPassword, state); err != nil { - return fmt.Errorf("greeter wallpaper override sync failed: %w", err) - } - - currentUser, err := user.Current() - if err != nil { - return fmt.Errorf("failed to resolve syncing user for per-user greeter cache: %w", err) - } - if err := syncUserGreeterCacheSlot(homeDir, cacheDir, currentUser.Username, state, logFunc, userSlotSyncOpts{ - sudoPassword: sudoPassword, - }); err != nil { - return fmt.Errorf("per-user greeter cache sync failed: %w", err) - } - - if err := SyncGreetdAutoLogin(cacheDir, homeDir, logFunc, sudoPassword); err != nil { - logFunc(fmt.Sprintf("⚠ Warning: greeter auto-login sync failed: %v", err)) - } - - if strings.ToLower(compositor) != "niri" { - return nil - } - - if err := syncNiriGreeterConfig(logFunc, sudoPassword); err != nil { - logFunc(fmt.Sprintf("⚠ Warning: Failed to sync niri greeter config: %v", err)) - } - - return nil -} - -func syncGreeterWallpaperOverride(cacheDir string, logFunc func(string), sudoPassword string, state greeterThemeSyncState) error { - destPath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg") - if state.ResolvedGreeterWallpaperPath == "" { - if err := privesc.Run(context.Background(), sudoPassword, "rm", "-f", destPath); err != nil { - return fmt.Errorf("failed to clear override file %s: %w", destPath, err) - } - logFunc("βœ“ Cleared greeter wallpaper override") - return nil - } - if err := privesc.Run(context.Background(), sudoPassword, "rm", "-f", destPath); err != nil { - return fmt.Errorf("failed to remove old override file %s: %w", destPath, err) - } - src := state.ResolvedGreeterWallpaperPath - st, err := os.Stat(src) - if err != nil { - return fmt.Errorf("configured greeter wallpaper not found at %s: %w", src, err) - } - if st.IsDir() { - return fmt.Errorf("configured greeter wallpaper path points to a directory: %s", src) - } - if err := privesc.Run(context.Background(), sudoPassword, "cp", src, destPath); err != nil { - return fmt.Errorf("failed to copy override wallpaper to %s: %w", destPath, err) - } - greeterGroup := DetectGreeterGroup() - daemonUser := DetectGreeterUser() - if err := privesc.Run(context.Background(), sudoPassword, "chown", daemonUser+":"+greeterGroup, destPath); err != nil { - if fallbackErr := privesc.Run(context.Background(), sudoPassword, "chown", "root:"+greeterGroup, destPath); fallbackErr != nil { - return fmt.Errorf("failed to set override ownership on %s: %w", destPath, err) - } - } - if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", destPath); err != nil { - return fmt.Errorf("failed to set override permissions on %s: %w", destPath, err) - } - logFunc("βœ“ Synced greeter wallpaper override") - return nil -} - -type niriGreeterSync struct { - processed map[string]bool - nodes []*document.Node - inputCount int - outputCount int - cursorCount int - debugCount int - cursorNode *document.Node - inputNode *document.Node - outputNodes map[string]*document.Node -} - -func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error { - configDir, err := os.UserConfigDir() - if err != nil { - return fmt.Errorf("failed to resolve user config directory: %w", err) - } - - configPath := filepath.Join(configDir, "niri", "config.kdl") - if _, err := os.Stat(configPath); os.IsNotExist(err) { - logFunc("β„Ή Niri config not found; skipping greeter niri sync") - return nil - } else if err != nil { - return fmt.Errorf("failed to stat niri config: %w", err) - } - - extractor := &niriGreeterSync{ - processed: make(map[string]bool), - outputNodes: make(map[string]*document.Node), - } - - if err := extractor.processFile(configPath); err != nil { - return err - } - - if len(extractor.nodes) == 0 { - logFunc("β„Ή No niri input/output sections found; skipping greeter niri sync") - return nil - } - - content := extractor.render() - if strings.TrimSpace(content) == "" { - logFunc("β„Ή No niri input/output content to sync; skipping greeter niri sync") - return nil - } - - greeterDir := "/etc/greetd/niri" - greeterGroup := DetectGreeterGroup() - if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", greeterDir); err != nil { - return fmt.Errorf("failed to create greetd niri directory: %w", err) - } - if err := privesc.Run(context.Background(), sudoPassword, "chown", fmt.Sprintf("root:%s", greeterGroup), greeterDir); err != nil { - return fmt.Errorf("failed to set greetd niri directory ownership: %w", err) - } - if err := privesc.Run(context.Background(), sudoPassword, "chmod", "755", greeterDir); err != nil { - return fmt.Errorf("failed to set greetd niri directory permissions: %w", err) - } - - dmsTemp, err := os.CreateTemp("", "dms-greeter-niri-dms-*.kdl") - if err != nil { - return fmt.Errorf("failed to create temp file: %w", err) - } - defer os.Remove(dmsTemp.Name()) - - if _, err := dmsTemp.WriteString(content); err != nil { - _ = dmsTemp.Close() - return fmt.Errorf("failed to write temp niri config: %w", err) - } - if err := dmsTemp.Close(); err != nil { - return fmt.Errorf("failed to close temp niri config: %w", err) - } - - dmsPath := filepath.Join(greeterDir, "dms.kdl") - if err := backupFileIfExists(sudoPassword, dmsPath, ".backup"); err != nil { - return fmt.Errorf("failed to backup %s: %w", dmsPath, err) - } - if err := privesc.Run(context.Background(), sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", dmsTemp.Name(), dmsPath); err != nil { - return fmt.Errorf("failed to install greetd niri dms config: %w", err) - } - - mainContent := fmt.Sprintf("%s\ninclude \"%s\"\n", config.NiriGreeterConfig, dmsPath) - mainTemp, err := os.CreateTemp("", "dms-greeter-niri-main-*.kdl") - if err != nil { - return fmt.Errorf("failed to create temp file: %w", err) - } - defer os.Remove(mainTemp.Name()) - - if _, err := mainTemp.WriteString(mainContent); err != nil { - _ = mainTemp.Close() - return fmt.Errorf("failed to write temp niri main config: %w", err) - } - if err := mainTemp.Close(); err != nil { - return fmt.Errorf("failed to close temp niri main config: %w", err) - } - - mainPath := filepath.Join(greeterDir, "config.kdl") - if err := backupFileIfExists(sudoPassword, mainPath, ".backup"); err != nil { - return fmt.Errorf("failed to backup %s: %w", mainPath, err) - } - if err := privesc.Run(context.Background(), sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", mainTemp.Name(), mainPath); err != nil { - return fmt.Errorf("failed to install greetd niri main config: %w", err) - } - - if err := ensureGreetdNiriConfig(logFunc, sudoPassword, mainPath); err != nil { - logFunc(fmt.Sprintf("⚠ Warning: Failed to update greetd config for niri: %v", err)) - } - - logFunc(fmt.Sprintf("βœ“ Synced niri greeter config (%d input, %d output, %d cursor, %d debug) to %s", extractor.inputCount, extractor.outputCount, extractor.cursorCount, extractor.debugCount, dmsPath)) - return nil -} - -func ensureGreetdNiriConfig(logFunc func(string), sudoPassword string, niriConfigPath string) error { - configPath := "/etc/greetd/config.toml" - data, err := os.ReadFile(configPath) - if os.IsNotExist(err) { - logFunc("β„Ή greetd config not found; skipping niri config wiring") - return nil - } - if err != nil { - return fmt.Errorf("failed to read greetd config: %w", err) - } - - lines := strings.Split(string(data), "\n") - updated := false - for i, line := range lines { - trimmed := strings.TrimSpace(line) - if !strings.HasPrefix(trimmed, "command") { - continue - } - - parts := strings.SplitN(trimmed, "=", 2) - if len(parts) != 2 { - continue - } - - command := strings.Trim(strings.TrimSpace(parts[1]), "\"") - if !strings.Contains(command, "dms-greeter") { - continue - } - if !strings.Contains(command, "--command niri") { - continue - } - command = stripConfigFlag(command) - command = stripCacheDirFlag(command) - command = strings.TrimSpace(command + " --cache-dir " + GreeterCacheDir) - - newCommand := fmt.Sprintf("%s -C %s", command, niriConfigPath) - idx := strings.Index(line, "command") - leading := "" - if idx > 0 { - leading = line[:idx] - } - lines[i] = fmt.Sprintf("%scommand = \"%s\"", leading, newCommand) - updated = true - break - } - - if !updated { - return nil - } - - tmpFile, err := os.CreateTemp("", "greetd-config-*.toml") - if err != nil { - return fmt.Errorf("failed to create temp greetd config: %w", err) - } - defer os.Remove(tmpFile.Name()) - - if _, err := tmpFile.WriteString(strings.Join(lines, "\n")); err != nil { - _ = tmpFile.Close() - return fmt.Errorf("failed to write temp greetd config: %w", err) - } - if err := tmpFile.Close(); err != nil { - return fmt.Errorf("failed to close temp greetd config: %w", err) - } - - if err := privesc.Run(context.Background(), sudoPassword, "mv", tmpFile.Name(), configPath); err != nil { - return fmt.Errorf("failed to update greetd config: %w", err) - } - - logFunc(fmt.Sprintf("βœ“ Updated greetd config to use niri config %s", niriConfigPath)) - return nil -} - -func backupFileIfExists(sudoPassword string, path string, suffix string) error { - if _, err := os.Stat(path); os.IsNotExist(err) { - return nil - } else if err != nil { - return err - } - - backupPath := fmt.Sprintf("%s%s-%s", path, suffix, time.Now().Format("20060102-150405")) - if err := privesc.Run(context.Background(), sudoPassword, "cp", path, backupPath); err != nil { - return err - } - return privesc.Run(context.Background(), sudoPassword, "chmod", "644", backupPath) -} - -func (s *niriGreeterSync) processFile(filePath string) error { - absPath, err := filepath.Abs(filePath) - if err != nil { - return fmt.Errorf("failed to resolve path %s: %w", filePath, err) - } - - if s.processed[absPath] { - return nil - } - s.processed[absPath] = true - - data, err := os.ReadFile(absPath) - if err != nil { - return fmt.Errorf("failed to read %s: %w", absPath, err) - } - - doc, err := kdl.Parse(strings.NewReader(string(data))) - if err != nil { - return fmt.Errorf("failed to parse KDL in %s: %w", absPath, err) - } - - baseDir := filepath.Dir(absPath) - for _, node := range doc.Nodes { - name := node.Name.String() - switch name { - case "include": - if err := s.handleInclude(node, baseDir); err != nil { - return err - } - case "input": - if s.inputNode == nil { - s.inputNode = node - s.inputNode.Children = dedupeCursorChildren(s.inputNode.Children) - s.nodes = append(s.nodes, node) - } else if len(node.Children) > 0 { - s.inputNode.Children = mergeInputChildren(s.inputNode.Children, node.Children) - } - s.inputCount++ - case "output": - key := outputNodeKey(node) - if existing, ok := s.outputNodes[key]; ok { - *existing = *node - } else { - s.outputNodes[key] = node - s.nodes = append(s.nodes, node) - } - s.outputCount++ - case "cursor": - if s.cursorNode == nil { - s.cursorNode = node - s.cursorNode.Children = dedupeCursorChildren(s.cursorNode.Children) - s.nodes = append(s.nodes, node) - s.cursorCount++ - } else if len(node.Children) > 0 { - s.cursorNode.Children = mergeCursorChildren(s.cursorNode.Children, node.Children) - } - case "debug": - s.nodes = append(s.nodes, node) - s.debugCount++ - } - } - - return nil -} - -func mergeCursorChildren(existing []*document.Node, incoming []*document.Node) []*document.Node { - if len(incoming) == 0 { - return existing - } - - indexByName := make(map[string]int, len(existing)) - for i, child := range existing { - indexByName[child.Name.String()] = i - } - - for _, child := range incoming { - name := child.Name.String() - if idx, ok := indexByName[name]; ok { - existing[idx] = child - continue - } - indexByName[name] = len(existing) - existing = append(existing, child) - } - - return existing -} - -func dedupeCursorChildren(children []*document.Node) []*document.Node { - if len(children) == 0 { - return children - } - - var result []*document.Node - indexByName := make(map[string]int, len(children)) - for _, child := range children { - name := child.Name.String() - if idx, ok := indexByName[name]; ok { - result[idx] = child - continue - } - indexByName[name] = len(result) - result = append(result, child) - } - - return result -} - -func mergeInputChildren(existing []*document.Node, incoming []*document.Node) []*document.Node { - if len(incoming) == 0 { - return existing - } - - indexByName := make(map[string]int, len(existing)) - for i, child := range existing { - indexByName[child.Name.String()] = i - } - - for _, child := range incoming { - name := child.Name.String() - if idx, ok := indexByName[name]; ok { - existing[idx] = child - continue - } - indexByName[name] = len(existing) - existing = append(existing, child) - } - - return existing -} - -func outputNodeKey(node *document.Node) string { - if len(node.Arguments) > 0 { - return strings.Trim(node.Arguments[0].String(), "\"") - } - return "" -} - -func (s *niriGreeterSync) handleInclude(node *document.Node, baseDir string) error { - if len(node.Arguments) == 0 { - return nil - } - - includePath := strings.Trim(node.Arguments[0].String(), "\"") - if includePath == "" { - return nil - } - - fullPath := includePath - if !filepath.IsAbs(includePath) { - fullPath = filepath.Join(baseDir, includePath) - } - - if _, err := os.Stat(fullPath); os.IsNotExist(err) { - return nil - } else if err != nil { - return fmt.Errorf("failed to stat include %s: %w", fullPath, err) - } - - return s.processFile(fullPath) -} - -func (s *niriGreeterSync) render() string { - if len(s.nodes) == 0 { - return "" - } - - var builder strings.Builder - for _, node := range s.nodes { - _, _ = node.WriteToOptions(&builder, document.NodeWriteOptions{ - LeadingTrailingSpace: true, - NameAndType: true, - Depth: 0, - Indent: []byte(" "), - IgnoreFlags: false, - }) - builder.WriteString("\n") - } - - return builder.String() -} - -func ConfigureGreetd(dmsPath, compositor string, logFunc func(string), sudoPassword string) error { - configPath := "/etc/greetd/config.toml" - - backupPath := fmt.Sprintf("%s.backup-%s", configPath, time.Now().Format("20060102-150405")) - if err := backupFileIfExists(sudoPassword, configPath, ".backup"); err != nil { - return fmt.Errorf("failed to backup greetd config: %w", err) - } - if _, err := os.Stat(configPath); err == nil { - logFunc(fmt.Sprintf("βœ“ Backed up existing config to %s", backupPath)) - } - - greeterUser := DetectGreeterUser() - - var configContent string - if data, err := os.ReadFile(configPath); err == nil { - configContent = string(data) - } else if os.IsNotExist(err) { - configContent = `[terminal] -vt = 1 - -[default_session] -` - } else { - return fmt.Errorf("failed to read greetd config: %w", err) - } - - commandValue := buildGreetdCommand(resolveGreeterWrapperPath(), compositor, dmsPath, IsVoidLinux()) - - commandLine := fmt.Sprintf(`command = "%s"`, commandValue) - newConfig := upsertDefaultSession(configContent, greeterUser, commandLine) - - if err := writeGreetdConfig(configPath, newConfig, logFunc, sudoPassword, fmt.Sprintf("βœ“ Updated greetd configuration (user: %s, command: %s)", greeterUser, commandValue)); err != nil { - return err - } - - return nil -} - -func buildGreetdCommand(wrapperCmd, compositor, dmsPath string, useVoidLogind bool) string { - commandValue := fmt.Sprintf("%s --command %s --cache-dir %s", wrapperCmd, strings.ToLower(compositor), GreeterCacheDir) - if dmsPath != "" { - commandValue = fmt.Sprintf("%s -p %s", commandValue, dmsPath) - } - if useVoidLogind { - commandValue = "env LIBSEAT_BACKEND=logind DMS_VOID=1 " + commandValue - } - return commandValue -} - -// EnsureVoidLogindGreetdCommand migrates DMS greeter commands on Void. -func EnsureVoidLogindGreetdCommand(logFunc func(string), sudoPassword string) error { - if !IsVoidLinux() { - return nil - } - - const configPath = "/etc/greetd/config.toml" - data, err := os.ReadFile(configPath) - if os.IsNotExist(err) { - return nil - } - if err != nil { - return fmt.Errorf("failed to read greetd config: %w", err) - } - - configContent := string(data) - command := extractDefaultSessionCommand(configContent) - if command == "" || !strings.Contains(command, "dms-greeter") { - return nil - } - - migratedCommand := voidLogindGreeterCommand(command) - if migratedCommand == command { - return nil - } - - greeterUser := extractDefaultSessionUser(configContent) - if greeterUser == "" { - greeterUser = DetectGreeterUser() - } - newConfig := upsertDefaultSession(configContent, greeterUser, fmt.Sprintf(`command = "%s"`, migratedCommand)) - return writeGreetdConfig(configPath, newConfig, logFunc, sudoPassword, "βœ“ Updated existing Void greeter to use elogind") -} - -func extractDefaultSessionCommand(configContent string) string { - inDefaultSession := false - for line := range strings.SplitSeq(configContent, "\n") { - if section, ok := parseTomlSection(line); ok { - inDefaultSession = section == "default_session" - continue - } - if !inDefaultSession { - continue - } - - trimmed := stripTomlComment(line) - if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") { - continue - } - parts := strings.SplitN(trimmed, "=", 2) - if len(parts) != 2 { - continue - } - if command := strings.Trim(strings.TrimSpace(parts[1]), `"`); command != "" { - return command - } - } - return "" -} - -func voidLogindGreeterCommand(command string) string { - if strings.Contains(command, "LIBSEAT_BACKEND=logind") && strings.Contains(command, "DMS_VOID=1") { - return command - } - return "env LIBSEAT_BACKEND=logind DMS_VOID=1 " + command -} - -func stripConfigFlag(command string) string { - for _, flag := range []string{" -C ", " --config "} { - idx := strings.Index(command, flag) - if idx == -1 { - continue - } - - before := command[:idx] - after := command[idx+len(flag):] - - switch { - case strings.HasPrefix(after, `"`): - if end := strings.Index(after[1:], `"`); end != -1 { - after = after[end+2:] - } else { - after = "" - } - default: - if space := strings.Index(after, " "); space != -1 { - after = after[space:] - } else { - after = "" - } - } - - command = strings.TrimSpace(before + after) - } - - return command -} - -func stripCacheDirFlag(command string) string { - fields := strings.Fields(command) - if len(fields) == 0 { - return strings.TrimSpace(command) - } - - filtered := make([]string, 0, len(fields)) - for i := 0; i < len(fields); i++ { - token := fields[i] - if token == "--cache-dir" { - if i+1 < len(fields) { - i++ - } - continue - } - if strings.HasPrefix(token, "--cache-dir=") { - continue - } - filtered = append(filtered, token) - } - - return strings.Join(filtered, " ") -} - -// getDebianOBSSlug returns the OBS repository slug for the running Debian version. -func getDebianOBSSlug(osInfo *distros.OSInfo) string { - versionID := strings.ToLower(osInfo.VersionID) - codename := strings.ToLower(osInfo.VersionCodename) - prettyName := strings.ToLower(osInfo.PrettyName) - - if strings.Contains(prettyName, "sid") || strings.Contains(prettyName, "unstable") || - codename == "sid" || versionID == "sid" { - return "Debian_Unstable" - } - if versionID == "testing" || codename == "testing" { - return "Debian_Testing" - } - if versionID != "" { - return "Debian_" + versionID // "Debian_13" - } - return "Debian_Unstable" -} - -// getOpenSUSEOBSRepoURL returns the OBS .repo file URL for the running openSUSE variant. -func getOpenSUSEOBSRepoURL(osInfo *distros.OSInfo) string { - const base = "https://download.opensuse.org/repositories/home:AvengeMedia:danklinux" - var slug string - switch osInfo.Distribution.ID { - case "opensuse-leap": - v := osInfo.VersionID - if v != "" && !strings.Contains(v, ".") { - v += ".0" // "16" β†’ "16.0" - } - if v == "" { - v = "16.0" - } - slug = v - case "opensuse-slowroll": - slug = "openSUSE_Slowroll" - default: // opensuse-tumbleweed || unknown version - slug = "openSUSE_Tumbleweed" - } - return fmt.Sprintf("%s/%s/home:AvengeMedia:danklinux.repo", base, slug) -} - -func checkSystemdEnabled(service string) (string, error) { - cmd := exec.Command("systemctl", "is-enabled", service) - output, _ := cmd.Output() - return strings.TrimSpace(string(output)), nil -} - -func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)) error { - conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"} - for _, dm := range conflictingDMs { - if isRunit() { - if !runitServiceEnabled(dm) { - continue - } - logFunc(fmt.Sprintf("Disabling conflicting display manager: %s", dm)) - if err := disableRunitService(dm, sudoPassword); err != nil { - logFunc(fmt.Sprintf("⚠ Warning: Failed to disable %s: %v", dm, err)) - } else { - logFunc(fmt.Sprintf("βœ“ Disabled %s", dm)) - } - continue - } - - state, err := checkSystemdEnabled(dm) - if err != nil || state == "" || state == "not-found" { - continue - } - switch state { - case "enabled", "enabled-runtime", "static", "indirect", "alias": - logFunc(fmt.Sprintf("Disabling conflicting display manager: %s", dm)) - if err := privesc.Run(context.Background(), sudoPassword, "systemctl", "disable", dm); err != nil { - logFunc(fmt.Sprintf("⚠ Warning: Failed to disable %s: %v", dm, err)) - } else { - logFunc(fmt.Sprintf("βœ“ Disabled %s", dm)) - } - } - } - return nil -} - -// EnableGreetd unmasks and enables greetd, forcing it over any other DM. -func EnableGreetd(sudoPassword string, logFunc func(string)) error { - if isRunit() { - if !runitServiceInstalled("greetd") { - return fmt.Errorf("greetd service not found in %s; ensure greetd is installed", runitSvDir) - } - if IsVoidLinux() { - ensureVoidLogindGreeter(DetectGreeterUser(), sudoPassword, logFunc) - } else { - ensureRunitSeat(DetectGreeterUser(), sudoPassword, logFunc) - } - ensureGreetdPamRundir(sudoPassword, logFunc) - if err := enableRunitService("greetd", sudoPassword); err != nil { - return fmt.Errorf("failed to enable greetd: %w", err) - } - logFunc(fmt.Sprintf("βœ“ greetd enabled (%s)", runitServiceDir)) - return nil - } - - state, err := checkSystemdEnabled("greetd") - if err != nil { - return fmt.Errorf("failed to check greetd state: %w", err) - } - if state == "not-found" { - return fmt.Errorf("greetd service not found; ensure greetd is installed") - } - if state == "masked" || state == "masked-runtime" { - logFunc(" Unmasking greetd...") - if err := privesc.Run(context.Background(), sudoPassword, "systemctl", "unmask", "greetd"); err != nil { - return fmt.Errorf("failed to unmask greetd: %w", err) - } - logFunc(" βœ“ Unmasked greetd") - } - logFunc(" Enabling greetd service (--force)...") - if err := privesc.Run(context.Background(), sudoPassword, "systemctl", "enable", "--force", "greetd"); err != nil { - return fmt.Errorf("failed to enable greetd: %w", err) - } - logFunc("βœ“ greetd enabled") - return nil -} - -func EnsureGraphicalTarget(sudoPassword string, logFunc func(string)) error { - if isRunit() { - logFunc("βœ“ runit detected; no graphical target is needed") - return nil - } - - cmd := exec.Command("systemctl", "get-default") - output, err := cmd.Output() - if err != nil { - logFunc(fmt.Sprintf("⚠ Warning: could not get default systemd target: %v", err)) - return nil - } - current := strings.TrimSpace(string(output)) - if current == "graphical.target" { - logFunc("βœ“ Default target is already graphical.target") - return nil - } - logFunc(fmt.Sprintf(" Setting default target to graphical.target (was: %s)...", current)) - if err := privesc.Run(context.Background(), sudoPassword, "systemctl", "set-default", "graphical.target"); err != nil { - return fmt.Errorf("failed to set graphical target: %w", err) - } - logFunc("βœ“ Default target set to graphical.target") - return nil -} - -// AutoSetupGreeter performs the full non-interactive greeter setup -func AutoSetupGreeter(compositor, sudoPassword string, logFunc func(string)) error { - if IsGreeterPackaged() && HasLegacyLocalGreeterWrapper() { - return fmt.Errorf("legacy manual wrapper detected at /usr/local/bin/dms-greeter; " + - "remove it before using packaged dms-greeter: sudo rm -f /usr/local/bin/dms-greeter") - } - - logFunc("Ensuring greetd is installed...") - if err := EnsureGreetdInstalled(logFunc, sudoPassword); err != nil { - return fmt.Errorf("greetd install failed: %w", err) - } - - dmsPath := "" - if !IsGreeterPackaged() { - detected, err := DetectDMSPath() - if err != nil { - return fmt.Errorf("DMS installation not found: %w", err) - } - dmsPath = detected - logFunc(fmt.Sprintf("βœ“ Found DMS at: %s", dmsPath)) - } else { - logFunc("βœ“ Using packaged dms-greeter (/usr/share/quickshell/dms-greeter)") - } - - logFunc("Setting up dms-greeter group and permissions...") - if err := SetupDMSGroup(logFunc, sudoPassword); err != nil { - logFunc(fmt.Sprintf("⚠ Warning: group/permissions setup error: %v", err)) - } - - logFunc("Copying greeter files...") - if err := CopyGreeterFiles(dmsPath, compositor, logFunc, sudoPassword); err != nil { - return fmt.Errorf("failed to copy greeter files: %w", err) - } - - logFunc("Configuring greetd...") - greeterPathForConfig := "" - if !IsGreeterPackaged() { - greeterPathForConfig = dmsPath - } - if err := ConfigureGreetd(greeterPathForConfig, compositor, logFunc, sudoPassword); err != nil { - return fmt.Errorf("failed to configure greetd: %w", err) - } - - logFunc("Synchronizing DMS configurations...") - if err := SyncDMSConfigs(dmsPath, compositor, logFunc, sudoPassword); err != nil { - logFunc(fmt.Sprintf("⚠ Warning: config sync error: %v", err)) - } - - logFunc("Configuring authentication...") - if err := sharedpam.SyncAuthConfig(logFunc, sudoPassword, sharedpam.SyncAuthOptions{}); err != nil { - return fmt.Errorf("failed to sync authentication: %w", err) - } - - logFunc("Checking for conflicting display managers...") - if err := DisableConflictingDisplayManagers(sudoPassword, logFunc); err != nil { - logFunc(fmt.Sprintf("⚠ Warning: %v", err)) - } - - logFunc("Enabling greetd service...") - if err := EnableGreetd(sudoPassword, logFunc); err != nil { - return fmt.Errorf("failed to enable greetd: %w", err) - } - - logFunc("Ensuring graphical.target as default...") - if err := EnsureGraphicalTarget(sudoPassword, logFunc); err != nil { - logFunc(fmt.Sprintf("⚠ Warning: %v", err)) - } - - logFunc("βœ“ DMS greeter setup complete") - return nil -} diff --git a/core/internal/greeter/installer_test.go b/core/internal/greeter/installer_test.go deleted file mode 100644 index 8f02f226d..000000000 --- a/core/internal/greeter/installer_test.go +++ /dev/null @@ -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)) - } -} diff --git a/core/internal/greeter/session_launcher.go b/core/internal/greeter/session_launcher.go deleted file mode 100644 index 0fa33b3e4..000000000 --- a/core/internal/greeter/session_launcher.go +++ /dev/null @@ -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) -} diff --git a/core/internal/greeter/session_launcher_test.go b/core/internal/greeter/session_launcher_test.go deleted file mode 100644 index 71dc05951..000000000 --- a/core/internal/greeter/session_launcher_test.go +++ /dev/null @@ -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") - } -} diff --git a/core/internal/greeter/user_cache_sync.go b/core/internal/greeter/user_cache_sync.go deleted file mode 100644 index 7e84faf62..000000000 --- a/core/internal/greeter/user_cache_sync.go +++ /dev/null @@ -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// -// 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 -} diff --git a/core/internal/greeter/user_cache_sync_test.go b/core/internal/greeter/user_cache_sync_test.go deleted file mode 100644 index 9d7b4a68a..000000000 --- a/core/internal/greeter/user_cache_sync_test.go +++ /dev/null @@ -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"]) - } -} diff --git a/core/internal/headless/runner.go b/core/internal/headless/runner.go index f7bc8903d..e5f664c3e 100644 --- a/core/internal/headless/runner.go +++ b/core/internal/headless/runner.go @@ -9,8 +9,8 @@ import ( "github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "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/utils" ) // 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) if !disabledItems["dms-greeter"] && r.depExists(dependencies, "dms-greeter") { - compositorName := "niri" - if wm == deps.WindowManagerHyprland { - compositorName = "Hyprland" - } fmt.Fprintln(os.Stdout, "Configuring DMS greeter...") logFunc := func(line string) { r.log(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 fmt.Fprintf(os.Stderr, "Warning: greeter setup issue (non-fatal): %v\n", err) } diff --git a/core/internal/pam/pam.go b/core/internal/pam/pam.go index e90501e1b..80f23df1d 100644 --- a/core/internal/pam/pam.go +++ b/core/internal/pam/pam.go @@ -5,30 +5,20 @@ import ( "encoding/json" "fmt" "os" - "os/exec" "path/filepath" "strings" - "time" - "github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "github.com/AvengeMedia/DankMaterialShell/core/internal/privesc" "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" ) 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)" LockscreenPamManagedBlockEnd = "# END DMS LOCKSCREEN AUTH" LockscreenU2FPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN U2F AUTH (managed by dms auth sync)" LockscreenU2FPamManagedBlockEnd = "# END DMS LOCKSCREEN U2F AUTH" - legacyGreeterPamFprintComment = "# DMS greeter fingerprint" - legacyGreeterPamU2FComment = "# DMS greeter U2F" - - GreetdPamPath = "/etc/pam.d/greetd" DankshellPamPath = "/etc/pam.d/dankshell" DankshellU2FPamPath = "/etc/pam.d/dankshell-u2f" ) @@ -74,31 +64,24 @@ var includedPamAuthFiles = []string{ } type AuthSettings struct { - EnableFprint bool `json:"enableFprint"` - EnableU2f bool `json:"enableU2f"` - GreeterEnableFprint bool `json:"greeterEnableFprint"` - GreeterEnableU2f bool `json:"greeterEnableU2f"` - GreeterPamExternallyManaged bool `json:"greeterPamExternallyManaged"` + EnableFprint bool `json:"enableFprint"` + EnableU2f bool `json:"enableU2f"` } type SyncAuthOptions struct { - HomeDir string - ForceGreeterAuth bool + HomeDir string } type syncDeps struct { - pamDir string - greetdPath string - dankshellPath string - dankshellU2fPath string - isNixOS func() bool - readFile func(string) ([]byte, error) - stat func(string) (os.FileInfo, error) - createTemp func(string, string) (*os.File, error) - removeFile func(string) error - runSudoCmd func(string, string, ...string) error - pamModuleExists func(string) bool - fingerprintAvailableForCurrentUser func() bool + pamDir string + dankshellPath string + dankshellU2fPath string + isNixOS func() bool + readFile func(string) ([]byte, error) + stat func(string) (os.FileInfo, error) + createTemp func(string, string) (*os.File, error) + removeFile func(string) error + runSudoCmd func(string, string, ...string) error } type lockscreenPamIncludeDirective struct { @@ -154,7 +137,6 @@ func (r lockscreenPamResolver) locate(target string) (string, error) { func defaultSyncDeps() syncDeps { return syncDeps{ pamDir: "/etc/pam.d", - greetdPath: GreetdPamPath, dankshellPath: DankshellPamPath, dankshellU2fPath: DankshellU2FPamPath, isNixOS: IsNixOS, @@ -165,8 +147,6 @@ func defaultSyncDeps() syncDeps { runSudoCmd: func(password, command string, args ...string) error { 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 } -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 { 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 { homeDir := strings.TrimSpace(options.HomeDir) if homeDir == "" { @@ -233,99 +201,9 @@ func syncAuthConfigWithDeps(logFunc func(string), sudoPassword string, options S 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 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 { lines := strings.Split(pamText, "\n") for _, line := range lines { @@ -1034,186 +912,6 @@ func syncLockscreenU2FPamConfigWithDeps(logFunc func(string), sudoPassword strin 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 { tmpFile, err := deps.createTemp("", "dms-pam-*.conf") if err != nil { @@ -1240,26 +938,6 @@ func writeManagedPamFile(content string, destPath string, sudoPassword string, d 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 { for _, libDir := range []string{ "/usr/lib64/security", @@ -1282,60 +960,3 @@ func pamModuleExists(module string) bool { } 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)) -} diff --git a/core/internal/pam/pam_test.go b/core/internal/pam/pam_test.go index 454799ebe..4c12fe7aa 100644 --- a/core/internal/pam/pam_test.go +++ b/core/internal/pam/pam_test.go @@ -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) { t.Helper() 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) } -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 { t.Helper() 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) { t.Parallel() @@ -1081,10 +900,98 @@ func containsSubstr(items []string, substr string) bool { 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) { 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() 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, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\naccount required pam_access.so\n") - var logs []string - err := syncAuthConfigWithDeps(func(msg string) { - logs = append(logs, msg) - }, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false)) + err := syncAuthConfigWithDeps(func(string) {}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false)) if err != nil { t.Fatalf("syncAuthConfigWithDeps returned error: %v", err) } @@ -1106,105 +1010,24 @@ func TestSyncAuthConfigWithDeps(t *testing.T) { if got := readFileString(t, env.dankshellU2fPath); got != buildManagedLockscreenU2FPamContent() { 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() env := newPamTestEnv(t) - env.availableModules["pam_fprintd.so"] = true - env.writeSettings(t, `{"enableU2f":false,"greeterEnableFprint":true,"greeterEnableU2f":false}`) + env.writeSettings(t, `{"enableU2f":false}`) 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, "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)) if err != nil { 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) { - t.Fatalf("expected dankshell-u2f to remain absent when enableU2f is false, 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) + t.Fatalf("expected dankshell-u2f to be removed, stat err = %v", err) } }) } diff --git a/core/internal/qmlchecks/greeter_auth_test.go b/core/internal/qmlchecks/greeter_auth_test.go deleted file mode 100644 index 98047a9ea..000000000 --- a/core/internal/qmlchecks/greeter_auth_test.go +++ /dev/null @@ -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) - } - } -} diff --git a/core/internal/tui/views_dependencies.go b/core/internal/tui/views_dependencies.go index e28945eb3..e7dd18722 100644 --- a/core/internal/tui/views_dependencies.go +++ b/core/internal/tui/views_dependencies.go @@ -7,7 +7,7 @@ import ( "github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "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" ) @@ -260,13 +260,6 @@ func (m Model) installPackages() tea.Cmd { // Run optional greeter setup if msg.Phase == distros.PhaseComplete && msg.IsComplete && msg.Error == nil { if m.optionalDepSelected("dms-greeter") { - compositorName := "niri" - switch m.selectedWindowManager() { - case deps.WindowManagerHyprland: - compositorName = "Hyprland" - case deps.WindowManagerMango: - compositorName = "mango" - } m.packageProgressChan <- packageInstallProgressMsg{ progress: 0.92, step: "Configuring DMS greeter...", @@ -279,7 +272,7 @@ func (m Model) installPackages() tea.Cmd { logOutput: line, } } - if err := greeter.AutoSetupGreeter(compositorName, m.sudoPassword, greeterLogFunc); err != nil { + if err := utils.RunDmsGreeterInstall(m.sudoPassword, greeterLogFunc); err != nil { m.packageProgressChan <- packageInstallProgressMsg{ progress: 0.96, step: "Greeter setup warning", diff --git a/core/internal/utils/greeter_setup.go b/core/internal/utils/greeter_setup.go new file mode 100644 index 000000000..ddaa763a5 --- /dev/null +++ b/core/internal/utils/greeter_setup.go @@ -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() +} diff --git a/dank-qml-common b/dank-qml-common index 493b609cc..c0f2d837e 160000 --- a/dank-qml-common +++ b/dank-qml-common @@ -1 +1 @@ -Subproject commit 493b609cc04659f1388f5738e8f65d6c531994fe +Subproject commit c0f2d837e1c9933e6789eeb38e7d456088bd5d1b diff --git a/distro/debian/dms-greeter/_service b/distro/debian/dms-greeter/_service deleted file mode 100644 index 80d5679ee..000000000 --- a/distro/debian/dms-greeter/_service +++ /dev/null @@ -1,9 +0,0 @@ - - - - https - github.com - /AvengeMedia/DankMaterialShell/releases/download/v1.4.3/dms-qml.tar.gz - dms-qml.tar.gz - - diff --git a/distro/debian/dms-greeter/debian/changelog b/distro/debian/dms-greeter/debian/changelog deleted file mode 100644 index 5115451d0..000000000 --- a/distro/debian/dms-greeter/debian/changelog +++ /dev/null @@ -1,5 +0,0 @@ -dms-greeter (1.4.3db1) unstable; urgency=medium - - * Update to v1.4.3 stable release - - -- Avenge Media Tue, 25 Feb 2026 02:40:00 +0000 diff --git a/distro/debian/dms-greeter/debian/control b/distro/debian/dms-greeter/debian/control deleted file mode 100644 index 216b5d989..000000000 --- a/distro/debian/dms-greeter/debian/control +++ /dev/null @@ -1,23 +0,0 @@ -Source: dms-greeter -Section: x11 -Priority: optional -Maintainer: Avenge Media -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. diff --git a/distro/debian/dms-greeter/debian/copyright b/distro/debian/dms-greeter/debian/copyright deleted file mode 100644 index 364012c9a..000000000 --- a/distro/debian/dms-greeter/debian/copyright +++ /dev/null @@ -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 -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. diff --git a/distro/debian/dms-greeter/debian/postinst b/distro/debian/dms-greeter/debian/postinst deleted file mode 100644 index 0e316cafd..000000000 --- a/distro/debian/dms-greeter/debian/postinst +++ /dev/null @@ -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 diff --git a/distro/debian/dms-greeter/debian/postrm b/distro/debian/dms-greeter/debian/postrm deleted file mode 100644 index 88addad3b..000000000 --- a/distro/debian/dms-greeter/debian/postrm +++ /dev/null @@ -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 diff --git a/distro/debian/dms-greeter/debian/rules b/distro/debian/dms-greeter/debian/rules deleted file mode 100644 index 80db8c384..000000000 --- a/distro/debian/dms-greeter/debian/rules +++ /dev/null @@ -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 diff --git a/distro/debian/dms-greeter/debian/source/format b/distro/debian/dms-greeter/debian/source/format deleted file mode 100644 index 89ae9db8f..000000000 --- a/distro/debian/dms-greeter/debian/source/format +++ /dev/null @@ -1 +0,0 @@ -3.0 (native) diff --git a/distro/debian/dms-greeter/debian/source/options b/distro/debian/dms-greeter/debian/source/options deleted file mode 100644 index 807343fc4..000000000 --- a/distro/debian/dms-greeter/debian/source/options +++ /dev/null @@ -1 +0,0 @@ -# OBS _service downloads dms-qml.tar.gz; no extra excludes needed diff --git a/distro/fedora/dms-greeter.spec b/distro/fedora/dms-greeter.spec deleted file mode 100644 index 51a8cc27f..000000000 --- a/distro/fedora/dms-greeter.spec +++ /dev/null @@ -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 - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER -- Stable release VERSION_PLACEHOLDER -- Built from GitHub release diff --git a/distro/nix/greeter.nix b/distro/nix/greeter.nix deleted file mode 100644 index 01e2d6b74..000000000 --- a/distro/nix/greeter.nix +++ /dev/null @@ -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" - ]; - }; -} diff --git a/distro/nix/tests/default.nix b/distro/nix/tests/default.nix index a22afdaed..3fc958b64 100644 --- a/distro/nix/tests/default.nix +++ b/distro/nix/tests/default.nix @@ -9,7 +9,6 @@ rec { paths = [ nixos-module nixos-service-start-module - greeter-niri-module niri-home-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 { inherit self diff --git a/distro/nix/tests/greeter-niri-module.nix b/distro/nix/tests/greeter-niri-module.nix deleted file mode 100644 index f98d5cea3..000000000 --- a/distro/nix/tests/greeter-niri-module.nix +++ /dev/null @@ -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) - ''; -} diff --git a/distro/opensuse/dms-greeter.spec b/distro/opensuse/dms-greeter.spec deleted file mode 100644 index eff998b7d..000000000 --- a/distro/opensuse/dms-greeter.spec +++ /dev/null @@ -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 - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER -- Stable release VERSION_PLACEHOLDER -- Initial OpenSUSE/OBS port from Fedora diff --git a/distro/scripts/copr-upload.sh b/distro/scripts/copr-upload.sh index e18b505de..31db344dd 100755 --- a/distro/scripts/copr-upload.sh +++ b/distro/scripts/copr-upload.sh @@ -5,7 +5,6 @@ set -euo pipefail # Usage: ./copr-upload.sh [PACKAGE] [VERSION] [RELEASE] # Examples: # ./copr-upload.sh dms 1.0.3 1 -# ./copr-upload.sh dms-greeter 1.0.3 1 PACKAGE="${1:-dms}" VERSION="${2:-}" @@ -17,11 +16,9 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" # Determine Copr project based on package if [ "$PACKAGE" = "dms" ]; then COPR_PROJECT="avengemedia/dms" -elif [ "$PACKAGE" = "dms-greeter" ]; then - COPR_PROJECT="avengemedia/danklinux" else echo "❌ Unknown package: $PACKAGE" - echo "Supported packages: dms, dms-greeter" + echo "Supported packages: dms" exit 1 fi diff --git a/distro/scripts/obs-status.sh b/distro/scripts/obs-status.sh index 8545ef25d..29439024c 100755 --- a/distro/scripts/obs-status.sh +++ b/distro/scripts/obs-status.sh @@ -11,7 +11,7 @@ OBS_BASE_PROJECT="home:AvengeMedia" OBS_BASE="$HOME/.cache/osc-checkouts" -ALL_PACKAGES=(dms dms-git dms-greeter) +ALL_PACKAGES=(dms dms-git) REPOS=("Debian_13" "openSUSE_Tumbleweed" "16.0") ARCHES=("x86_64" "aarch64") @@ -41,9 +41,6 @@ for pkg in "${PACKAGES[@]}"; do dms-git) PROJECT="$OBS_BASE_PROJECT:dms-git" ;; - dms-greeter) - PROJECT="$OBS_BASE_PROJECT:danklinux" - ;; *) echo "Error: Unknown package '$pkg'" continue diff --git a/distro/scripts/obs-upload.sh b/distro/scripts/obs-upload.sh index 85abc432c..0111f0f9b 100755 --- a/distro/scripts/obs-upload.sh +++ b/distro/scripts/obs-upload.sh @@ -68,14 +68,13 @@ fi OBS_BASE_PROJECT="home:AvengeMedia" OBS_BASE="$HOME/.cache/osc-checkouts" -AVAILABLE_PACKAGES=(dms dms-git dms-greeter) +AVAILABLE_PACKAGES=(dms dms-git) if [[ -z "$PACKAGE" ]]; then echo "Available packages:" echo "" echo " 1. dms - Stable DMS" echo " 2. dms-git - Nightly DMS" - echo " 3. dms-greeter - DMS greeter for greetd" echo " a. all" echo "" read -r -p "Select package (1-${#AVAILABLE_PACKAGES[@]}, a): " selection @@ -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" } -update_debian_dms_greeter_service() { - local service_path="$1" - if [[ -z "$service_path" || ! -f "$service_path" ]]; then - return 0 - fi - if [[ -z "$CHANGELOG_VERSION" ]]; then - return 0 - fi - local base_version - base_version=$(echo "$CHANGELOG_VERSION" | sed -E 's/^([0-9]+(\.[0-9]+)*).*/\1/') - if [[ -z "$base_version" ]]; then - return 0 - fi - sed -i "s|/releases/download/v[0-9][^\"]*/dms-qml\.tar\.gz|/releases/download/v${base_version}/dms-qml.tar.gz|" "$service_path" -} - update_opensuse_git_spec() { local spec_path="$1" local go_ver @@ -323,9 +306,6 @@ dms) dms-git) PROJECT="dms-git" ;; -dms-greeter) - PROJECT="danklinux" - ;; *) echo "Error: Unknown package '$PACKAGE'" exit 1 @@ -425,8 +405,6 @@ if [[ -d "distro/debian/$PACKAGE/debian" ]]; then # Keep Debian _service in sync with changelog version if [[ "$PACKAGE" == "dms" ]] && [[ -f "distro/debian/$PACKAGE/_service" ]]; then update_debian_dms_service "distro/debian/$PACKAGE/_service" - elif [[ "$PACKAGE" == "dms-greeter" ]] && [[ -f "distro/debian/$PACKAGE/_service" ]]; then - update_debian_dms_greeter_service "distro/debian/$PACKAGE/_service" fi # Check if this version already exists in OBS @@ -480,16 +458,6 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ -f "distro/opensuse/$PACKAGE.spec" ]]; if [[ "$PACKAGE" == *"-git" ]]; then update_opensuse_git_spec "$WORK_DIR/$PACKAGE.spec" - elif [[ "$PACKAGE" == "dms-greeter" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then - DMS_GREETER_BASE_VERSION=$(echo "$CHANGELOG_VERSION" | sed -E 's/^([0-9]+(\.[0-9]+)*).*/\1/') - DMS_GREETER_RELEASE=$(echo "$CHANGELOG_VERSION" | sed -E 's/.*db([0-9]+)$/\1/' || echo "1") - CHANGELOG_DATE=$(date '+%a %b %d %Y') - sed -i "s/VERSION_PLACEHOLDER/${DMS_GREETER_BASE_VERSION}/g" "$WORK_DIR/$PACKAGE.spec" - sed -i "s/RELEASE_PLACEHOLDER/${DMS_GREETER_RELEASE}/g" "$WORK_DIR/$PACKAGE.spec" - sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" "$WORK_DIR/$PACKAGE.spec" - # Explicitly set Version:/Release: in case the spec uses %{version} macro - sed -i "s/^Version:.*/Version: ${DMS_GREETER_BASE_VERSION}/" "$WORK_DIR/$PACKAGE.spec" - sed -i "s/^Release:.*/Release: ${DMS_GREETER_RELEASE}%{?dist}/" "$WORK_DIR/$PACKAGE.spec" fi if [[ -f "$WORK_DIR/.osc/$PACKAGE.spec" ]]; then @@ -555,24 +523,6 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ "$UPLOAD_DEBIAN" == false ]] && [[ -f fi fi - # For dms-greeter: download dms-qml.tar.gz from _service URL - if [[ -z "${SOURCE_DIR:-}" ]] && [[ "$PACKAGE" == "dms-greeter" ]] && [[ -f "distro/debian/$PACKAGE/_service" ]]; then - DMS_GREETER_URL=$(grep -A 5 'name="download_url"' "distro/debian/$PACKAGE/_service" | grep "path" | sed 's/.*\(.*\)<\/param>.*/\1/' | head -1) - if [[ -n "$DMS_GREETER_URL" ]]; then - DMS_GREETER_FULL_URL="https://github.com${DMS_GREETER_URL}" - echo " Downloading dms-greeter source from: $DMS_GREETER_FULL_URL" - if wget -q -O "$TEMP_DIR/dms-qml.tar.gz" "$DMS_GREETER_FULL_URL" 2>/dev/null || \ - curl -L -f -s -o "$TEMP_DIR/dms-qml.tar.gz" "$DMS_GREETER_FULL_URL" 2>/dev/null; then - cd "$TEMP_DIR" - tar -xzf dms-qml.tar.gz - if [[ -f "Modules/Greetd/assets/dms-greeter" ]]; then - SOURCE_DIR="$TEMP_DIR" - fi - cd "$REPO_ROOT" - fi - fi - fi - if [[ -n "$SOURCE_DIR" && -d "$SOURCE_DIR" ]]; then SOURCE0=$(grep "^Source0:" "distro/opensuse/$PACKAGE.spec" | awk '{print $2}' | head -1) @@ -581,15 +531,6 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ "$UPLOAD_DEBIAN" == false ]] && [[ -f cd "$OBS_TARBALL_DIR" case "$PACKAGE" in - dms-greeter) - EXPECTED_DIR="dms-qml" - echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)" - mkdir -p "$EXPECTED_DIR" - cp -a "$SOURCE_DIR"/. "$EXPECTED_DIR/" - tar -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_VERSION=$(grep "^Version:" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec" | sed 's/^Version:[[:space:]]*//' | head -1) EXPECTED_DIR="DankMaterialShell-${DMS_VERSION}" @@ -727,17 +668,6 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t if [[ -z "$SOURCE_DIR" ]]; then SOURCE_DIR=$(find . -maxdepth 1 -type d ! -name "." | head -1) fi - # dms-qml.tar.gz extracts flat (quickshell contents, no top-level dir) - # Create dms-qml wrapper so combined tarball has correct top-level dir (like dms) - if [[ "$PACKAGE" == "dms-greeter" ]] && [[ -f "Modules/Greetd/assets/dms-greeter" ]]; then - mkdir -p dms-qml - for f in *; do - if [[ -e "$f" && "$f" != "dms-qml" && "$f" != "source-archive" ]]; then - mv "$f" dms-qml/ 2>/dev/null || true - fi - done - SOURCE_DIR="dms-qml" - fi if [[ -z "$SOURCE_DIR" || ! -d "$SOURCE_DIR" ]]; then echo "Error: Failed to extract source archive or find source directory" echo "Contents of $TEMP_DIR:" @@ -814,21 +744,6 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t cd "$OBS_TARBALL_DIR" case "$PACKAGE" in - dms-greeter) - EXPECTED_DIR="dms-qml" - echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)" - mkdir -p "$EXPECTED_DIR" - cp -a "$SOURCE_DIR"/. "$EXPECTED_DIR/" - if [[ "$SOURCE0" == *.tar.xz ]]; then - tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -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_VERSION=$(grep "^Version:" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec" | sed 's/^Version:[[:space:]]*//' | head -1) 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/" if [[ "$PACKAGE" == *"-git" ]]; then update_opensuse_git_spec "$WORK_DIR/$PACKAGE.spec" - elif [[ "$PACKAGE" == "dms-greeter" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then - DMS_GREETER_BASE_VERSION=$(echo "$CHANGELOG_VERSION" | sed -E 's/^([0-9]+(\.[0-9]+)*).*/\1/') - DMS_GREETER_RELEASE=$(echo "$CHANGELOG_VERSION" | sed -E 's/.*db([0-9]+)$/\1/' || echo "1") - CHANGELOG_DATE=$(date '+%a %b %d %Y') - sed -i "s/VERSION_PLACEHOLDER/${DMS_GREETER_BASE_VERSION}/g" "$WORK_DIR/$PACKAGE.spec" - sed -i "s/RELEASE_PLACEHOLDER/${DMS_GREETER_RELEASE}/g" "$WORK_DIR/$PACKAGE.spec" - sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" "$WORK_DIR/$PACKAGE.spec" - # Explicitly set Version:/Release: in case the spec uses %{version} macro - sed -i "s/^Version:.*/Version: ${DMS_GREETER_BASE_VERSION}/" "$WORK_DIR/$PACKAGE.spec" - sed -i "s/^Release:.*/Release: ${DMS_GREETER_RELEASE}%{?dist}/" "$WORK_DIR/$PACKAGE.spec" fi fi @@ -1029,47 +934,10 @@ EOF echo " - Quilt format detected: creating debian.tar.gz" tar -czf "$WORK_DIR/debian.tar.gz" -C "distro/debian/$PACKAGE" debian/ - # For dms-greeter: create orig tarball so Debian build gets upstream (OBS only passes .dsc Files to Debian) - DSC_FILES_DEBIAN="" - if [[ "$PACKAGE" == "dms-greeter" ]]; then - UPSTREAM_VER=$(echo "$VERSION" | sed 's/-[^-]*$//') - ORIG_TARBALL="${PACKAGE}_${UPSTREAM_VER}.orig.tar.gz" - ORIG_DIR="${PACKAGE}-${UPSTREAM_VER}" - - if [[ -f "distro/debian/$PACKAGE/_service" ]] && grep -q "download_url" "distro/debian/$PACKAGE/_service"; then - DG_TEMP=$(mktemp -d) - DMS_GREETER_PATH=$(grep -A 5 'name="download_url"' "distro/debian/$PACKAGE/_service" | grep "path" | sed 's/.*\(.*\)<\/param>.*/\1/' | head -1) - if [[ -n "$DMS_GREETER_PATH" ]]; then - DG_URL="https://github.com${DMS_GREETER_PATH}" - echo " - Downloading dms-greeter source for orig tarball: $DG_URL" - if wget -q -O "$DG_TEMP/dms-qml.tar.gz" "$DG_URL" 2>/dev/null || curl -L -f -s -o "$DG_TEMP/dms-qml.tar.gz" "$DG_URL" 2>/dev/null; then - ( cd "$DG_TEMP" && tar --no-same-owner -xzf dms-qml.tar.gz && mkdir -p "$ORIG_DIR" && \ - for f in *; do [[ "$f" != "dms-qml.tar.gz" && "$f" != "$ORIG_DIR" ]] && mv "$f" "$ORIG_DIR/"; done ) - if [[ -d "$DG_TEMP/$ORIG_DIR/Modules" ]] || [[ -f "$DG_TEMP/$ORIG_DIR/LICENSE" ]]; then - tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$ORIG_TARBALL" -C "$DG_TEMP" "$ORIG_DIR" - ORIG_MD5=$(md5sum "$WORK_DIR/$ORIG_TARBALL" | cut -d' ' -f1) - ORIG_SIZE=$(stat -c%s "$WORK_DIR/$ORIG_TARBALL" 2>/dev/null || stat -f%z "$WORK_DIR/$ORIG_TARBALL" 2>/dev/null) - DSC_FILES_DEBIAN=" $ORIG_MD5 $ORIG_SIZE $ORIG_TARBALL -" - echo " - Created $ORIG_TARBALL for Debian orig" - fi - rm -rf "$DG_TEMP" - fi - fi - fi - fi - DEBIAN_MD5=$(md5sum "$WORK_DIR/debian.tar.gz" | cut -d' ' -f1) DEBIAN_SIZE=$(stat -c%s "$WORK_DIR/debian.tar.gz" 2>/dev/null || stat -f%z "$WORK_DIR/debian.tar.gz" 2>/dev/null) echo " - Generating $PACKAGE.dsc for quilt format" - # debtransform: DEBTRANSFORM-TAR = orig (upstream), DEBTRANSFORM-FILES-TAR = debian archive - DEBTRANSFORM_EXTRA="" - if [[ -n "$DSC_FILES_DEBIAN" ]] && [[ -n "$ORIG_TARBALL" ]]; then - DEBTRANSFORM_EXTRA="DEBTRANSFORM-TAR: $ORIG_TARBALL -DEBTRANSFORM-FILES-TAR: debian.tar.gz -" - fi cat >"$WORK_DIR/$PACKAGE.dsc" < Build-Depends: debhelper-compat (= 13), wget, gzip -${DEBTRANSFORM_EXTRA}Files:${DSC_FILES_DEBIAN} +Files: $DEBIAN_MD5 $DEBIAN_SIZE debian.tar.gz DSCEOF fi @@ -1117,13 +985,6 @@ if [[ -n "$OBS_FILES" ]]; then continue fi - # Keep current orig tarball for dms-greeter (Debian 3.0 quilt needs it) - UPSTREAM_VER_CLEAN=$(echo "$CHANGELOG_VERSION" | sed 's/-[^-]*$//' 2>/dev/null) - if [[ "$PACKAGE" == "dms-greeter" ]] && [[ "$old_file" == "${PACKAGE}_${UPSTREAM_VER_CLEAN}.orig.tar.gz" ]]; then - echo " - Keeping orig tarball: $old_file" - continue - fi - if [[ "$old_file" == "${PACKAGE}-source.tar.gz" ]]; then echo " - Keeping source tarball: $old_file" continue diff --git a/distro/scripts/ppa-build.sh b/distro/scripts/ppa-build.sh index 56cea48bc..9b58de7c9 100755 --- a/distro/scripts/ppa-build.sh +++ b/distro/scripts/ppa-build.sh @@ -118,7 +118,6 @@ get_ppa_name() { case "$pkg" in dms) echo "dms" ;; dms-git) echo "dms-git" ;; - dms-greeter) echo "danklinux" ;; *) echo "" ;; esac } @@ -227,9 +226,6 @@ dms-git) dms) GIT_REPO="AvengeMedia/DankMaterialShell" ;; -dms-greeter) - GIT_REPO="AvengeMedia/DankMaterialShell" - ;; danksearch) GIT_REPO="AvengeMedia/danksearch" ;; @@ -352,18 +348,6 @@ EOF 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 fi diff --git a/distro/scripts/ppa-status.sh b/distro/scripts/ppa-status.sh index 711b65a12..8cf4bab3f 100755 --- a/distro/scripts/ppa-status.sh +++ b/distro/scripts/ppa-status.sh @@ -14,7 +14,7 @@ LAUNCHPAD_API="https://api.launchpad.net/1.0" DISTRO_SERIES_LIST=(resolute stonking) # 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 get_ppa_name() { @@ -22,7 +22,6 @@ get_ppa_name() { case "$pkg" in dms) echo "dms" ;; dms-git) echo "dms-git" ;; - dms-greeter) echo "danklinux" ;; *) echo "" ;; esac } @@ -72,7 +71,7 @@ elif [[ -n "$PPA_INPUT" ]]; then else # Check all packages in all PPAs PACKAGES=("${ALL_PACKAGES[@]}") - PPAS=("dms" "dms-git" "danklinux") + PPAS=("dms" "dms-git") fi # Function to get build status color and symbol diff --git a/distro/scripts/ppa-sync-plan.sh b/distro/scripts/ppa-sync-plan.sh index 2c6dcc710..c4f668d23 100755 --- a/distro/scripts/ppa-sync-plan.sh +++ b/distro/scripts/ppa-sync-plan.sh @@ -13,7 +13,6 @@ JSON=false PACKAGES=( "dms:dms:release" "dms-git:dms-git:git" - "dms-greeter:danklinux:release" ) while [[ $# -gt 0 ]]; do diff --git a/distro/scripts/ppa-upload.sh b/distro/scripts/ppa-upload.sh index 7b1ace7e5..fc031fac3 100755 --- a/distro/scripts/ppa-upload.sh +++ b/distro/scripts/ppa-upload.sh @@ -27,7 +27,7 @@ success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } error() { echo -e "${RED}[ERROR]${NC} $1"; } -AVAILABLE_PACKAGES=(dms dms-git dms-greeter) +AVAILABLE_PACKAGES=(dms dms-git) KEEP_BUILDS=false REBUILD_RELEASE="" @@ -101,7 +101,6 @@ get_ppa_name() { case "$pkg" in dms) echo "dms" ;; dms-git) echo "dms-git" ;; - dms-greeter) echo "danklinux" ;; *) echo "" ;; esac } @@ -336,7 +335,7 @@ echo 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" BUILD_DIR=$(dirname "$CHANGES_FILE") @@ -410,9 +409,9 @@ EOF fi else # 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 "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 fi @@ -484,13 +483,6 @@ if [ "$KEEP_BUILDS" = "false" ]; then REMOVED=$((REMOVED + 1)) 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 if [ $REMOVED -gt 0 ]; then diff --git a/distro/ubuntu/dms-greeter/debian/changelog b/distro/ubuntu/dms-greeter/debian/changelog deleted file mode 100644 index cd77f6487..000000000 --- a/distro/ubuntu/dms-greeter/debian/changelog +++ /dev/null @@ -1,5 +0,0 @@ -dms-greeter (1.0.2ppa2) questing; urgency=medium - - * Rebuild for packaging fixes (ppa2) - - -- Avenge Media Sat, 13 Dec 2025 06:50:44 +0000 diff --git a/distro/ubuntu/dms-greeter/debian/control b/distro/ubuntu/dms-greeter/debian/control deleted file mode 100644 index 216b5d989..000000000 --- a/distro/ubuntu/dms-greeter/debian/control +++ /dev/null @@ -1,23 +0,0 @@ -Source: dms-greeter -Section: x11 -Priority: optional -Maintainer: Avenge Media -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. diff --git a/distro/ubuntu/dms-greeter/debian/copyright b/distro/ubuntu/dms-greeter/debian/copyright deleted file mode 100644 index cf9ece53f..000000000 --- a/distro/ubuntu/dms-greeter/debian/copyright +++ /dev/null @@ -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 -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. diff --git a/distro/ubuntu/dms-greeter/debian/postinst b/distro/ubuntu/dms-greeter/debian/postinst deleted file mode 100755 index 0e316cafd..000000000 --- a/distro/ubuntu/dms-greeter/debian/postinst +++ /dev/null @@ -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 diff --git a/distro/ubuntu/dms-greeter/debian/postrm b/distro/ubuntu/dms-greeter/debian/postrm deleted file mode 100755 index 88addad3b..000000000 --- a/distro/ubuntu/dms-greeter/debian/postrm +++ /dev/null @@ -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 diff --git a/distro/ubuntu/dms-greeter/debian/rules b/distro/ubuntu/dms-greeter/debian/rules deleted file mode 100755 index 3ba85c9bb..000000000 --- a/distro/ubuntu/dms-greeter/debian/rules +++ /dev/null @@ -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 diff --git a/distro/ubuntu/dms-greeter/debian/source/format b/distro/ubuntu/dms-greeter/debian/source/format deleted file mode 100644 index 89ae9db8f..000000000 --- a/distro/ubuntu/dms-greeter/debian/source/format +++ /dev/null @@ -1 +0,0 @@ -3.0 (native) diff --git a/distro/ubuntu/dms-greeter/debian/source/include-binaries b/distro/ubuntu/dms-greeter/debian/source/include-binaries deleted file mode 100644 index e95c5c0f1..000000000 --- a/distro/ubuntu/dms-greeter/debian/source/include-binaries +++ /dev/null @@ -1 +0,0 @@ -dms-greeter-source.tar.gz diff --git a/distro/ubuntu/dms-greeter/debian/source/options b/distro/ubuntu/dms-greeter/debian/source/options deleted file mode 100644 index d46bb4a3e..000000000 --- a/distro/ubuntu/dms-greeter/debian/source/options +++ /dev/null @@ -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 diff --git a/distro/void/README.md b/distro/void/README.md index 5d2f434a9..2d9e4426a 100644 --- a/distro/void/README.md +++ b/distro/void/README.md @@ -5,7 +5,6 @@ XBPS templates for DankMaterialShell on [Void Linux](https://voidlinux.org). | Package | Source repo | 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`) | | `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/` dirs in ./xbps-src pkg dgop ./xbps-src pkg danksearch ./xbps-src pkg dms -./xbps-src pkg dms-greeter # optional # lint (xlint ships in the xtools package) 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. - -## 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. diff --git a/distro/void/srcpkgs/dms-greeter/INSTALL.msg b/distro/void/srcpkgs/dms-greeter/INSTALL.msg deleted file mode 100644 index 692357aeb..000000000 --- a/distro/void/srcpkgs/dms-greeter/INSTALL.msg +++ /dev/null @@ -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). diff --git a/distro/void/srcpkgs/dms-greeter/template b/distro/void/srcpkgs/dms-greeter/template deleted file mode 100644 index 8623f1c7d..000000000 --- a/distro/void/srcpkgs/dms-greeter/template +++ /dev/null @@ -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 " -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 -} diff --git a/flake.lock b/flake.lock index a1e9ffa9d..691ef967c 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "dank-qml-common": { "flake": false, "locked": { - "lastModified": 1784471445, + "lastModified": 1784485083, "narHash": "sha256-hRTmcd3Ms9DF3X35ZiapXM15rG5X1DtIdlrQZwRINFE=", "owner": "AvengeMedia", "repo": "dank-qml-common", - "rev": "493b609cc04659f1388f5738e8f65d6c531994fe", + "rev": "c0f2d837e1c9933e6789eeb38e7d456088bd5d1b", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 5ef1fe6b1..f9a7a8b8d 100644 --- a/flake.nix +++ b/flake.nix @@ -159,9 +159,6 @@ --replace-fail /usr/bin/dms $out/bin/dms \ --replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill - substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \ - --replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash - substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \ --replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.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.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; diff --git a/quickshell/Common/CacheData.qml b/quickshell/Common/CacheData.qml index 70ef250a8..5e6ef5f04 100644 --- a/quickshell/Common/CacheData.qml +++ b/quickshell/Common/CacheData.qml @@ -13,8 +13,6 @@ Singleton { 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 _stateDir: Paths.strip(_stateUrl) @@ -75,9 +73,7 @@ Singleton { }) Component.onCompleted: { - if (!isGreeterMode) { - loadCache(); - } + loadCache(); } function loadCache() { @@ -200,7 +196,7 @@ Singleton { FileView { id: launcherCacheFile - path: isGreeterMode ? "" : _stateDir + "/DankMaterialShell/launcher_cache.json" + path: _stateDir + "/DankMaterialShell/launcher_cache.json" blockLoading: true blockWrites: true atomicWrites: true @@ -210,20 +206,16 @@ Singleton { FileView { id: cacheFile - path: isGreeterMode ? "" : _stateDir + "/DankMaterialShell/cache.json" + path: _stateDir + "/DankMaterialShell/cache.json" blockLoading: true blockWrites: true atomicWrites: true - watchChanges: !isGreeterMode + watchChanges: true onLoaded: { - if (!isGreeterMode) { - parseCache(cacheFile.text()); - } + parseCache(cacheFile.text()); } onLoadFailed: error => { - if (!isGreeterMode) { - log.info("No cache file found, starting fresh"); - } + log.info("No cache file found, starting fresh"); } } } diff --git a/quickshell/Common/LayoutCodes.js b/quickshell/Common/LayoutCodes.js deleted file mode 100644 index 9ab383129..000000000 --- a/quickshell/Common/LayoutCodes.js +++ /dev/null @@ -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(); -} diff --git a/quickshell/Common/SessionData.qml b/quickshell/Common/SessionData.qml index 5216872f2..95e6e239e 100644 --- a/quickshell/Common/SessionData.qml +++ b/quickshell/Common/SessionData.qml @@ -16,7 +16,6 @@ Singleton { 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 _hasLoaded: false property bool _isReadOnly: false @@ -227,9 +226,7 @@ Singleton { property string settingsSidebarCollapsedIds: "," Component.onCompleted: { - if (!isGreeterMode) { - loadSettings(); - } + loadSettings(); } property var _pendingMigration: null @@ -238,11 +235,6 @@ Singleton { _hasUnsavedChanges = false; _pendingMigration = null; - if (isGreeterMode) { - parseSettings(greeterSessionFile.text()); - return; - } - try { const txt = settingsFile.text(); let obj = (txt && txt.trim()) ? JSON.parse(txt) : null; @@ -280,7 +272,7 @@ Singleton { _loadedSessionSnapshot = getCurrentSessionJson(); _hasLoaded = true; - if (!isGreeterMode && typeof Theme !== "undefined") + if (typeof Theme !== "undefined") Theme.generateSystemThemesFromCurrentTheme(); if (typeof WallpaperCyclingService !== "undefined") @@ -362,7 +354,7 @@ Singleton { _loadedSessionSnapshot = getCurrentSessionJson(); _hasLoaded = true; - if (!isGreeterMode && typeof Theme !== "undefined") + if (typeof Theme !== "undefined") Theme.generateSystemThemesFromCurrentTheme(); if (typeof WallpaperCyclingService !== "undefined") @@ -386,7 +378,7 @@ Singleton { } function saveSettings() { - if (isGreeterMode || _parseError || !_hasLoaded) + if (_parseError || !_hasLoaded) return; settingsFile.setText(getCurrentSessionJson()); if (_isReadOnly) @@ -1394,16 +1386,14 @@ Singleton { FileView { id: settingsFile - path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json" + path: StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json" blockLoading: true blockWrites: true atomicWrites: true - watchChanges: !isGreeterMode + watchChanges: true onLoaded: { - if (!isGreeterMode) { - _hasUnsavedChanges = false; - parseSettings(settingsFile.text()); - } + _hasUnsavedChanges = false; + parseSettings(settingsFile.text()); } onSaveFailed: error => { root._isReadOnly = true; @@ -1411,39 +1401,6 @@ Singleton { } } - 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 { id: sessionWritableCheckProcess diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 1a0929166..b0b4e0a15 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -17,8 +17,6 @@ Singleton { readonly property int settingsConfigVersion: 12 - readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true" - enum Position { Top, Bottom, @@ -1439,19 +1437,15 @@ Singleton { signal workspaceIconsUpdated function refreshAuthAvailability() { - if (isGreeterMode) - return; Processes.detectAuthCapabilities(); } Component.onCompleted: { - if (!isGreeterMode) { - Processes.settingsRoot = root; - loadSettings(); - initializeListModels(); - refreshAuthAvailability(); - Processes.checkPluginSettings(); - } + Processes.settingsRoot = root; + loadSettings(); + initializeListModels(); + refreshAuthAvailability(); + Processes.checkPluginSettings(); } function applyStoredTheme() { @@ -1517,8 +1511,6 @@ Singleton { } function checkIconThemeDrift() { - if (isGreeterMode) - return; if (resolveIconTheme() === "System Default") return; if (!lastAppliedIconTheme) @@ -1674,8 +1666,6 @@ Singleton { } function scheduleAuthApply() { - if (isGreeterMode) - return; Qt.callLater(() => { Processes.settingsRoot = root; Processes.scheduleAuthApply(); @@ -1683,8 +1673,6 @@ Singleton { } function scheduleGreeterAutoLoginSync() { - if (isGreeterMode) - return; Qt.callLater(() => { Processes.settingsRoot = root; Processes.scheduleGreeterAutoLoginSync(); @@ -1692,8 +1680,6 @@ Singleton { } function markGreeterSyncPending(who, key, oldValue) { - if (isGreeterMode) - return; if (!(key in greeterSyncBaseline)) { var baseline = greeterSyncBaseline; baseline[key] = oldValue; @@ -3632,7 +3618,7 @@ Singleton { FileView { id: settingsFile - path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json" + path: StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json" blockLoading: true blockWrites: true atomicWrites: true @@ -3645,8 +3631,6 @@ Singleton { settingsFileReloadDebounce.restart(); } onLoaded: { - if (isGreeterMode) - return; const wasLoaded = _hasLoaded; const prevFrameEnabled = frameEnabled; const prevFrameMode = frameMode; @@ -3689,9 +3673,7 @@ Singleton { updateFrameCompositorLayout(); } onLoadFailed: error => { - if (!isGreeterMode) { - applyStoredTheme(); - } + applyStoredTheme(); } onSaveFailed: error => { root._isReadOnly = true; @@ -3702,24 +3684,20 @@ Singleton { FileView { id: pluginSettingsFile - path: isGreeterMode ? "" : pluginSettingsPath + path: pluginSettingsPath blockLoading: true blockWrites: true atomicWrites: true printErrors: false - watchChanges: !isGreeterMode + watchChanges: true onLoaded: { - if (!isGreeterMode) { - parsePluginSettings(pluginSettingsFile.text()); - } + parsePluginSettings(pluginSettingsFile.text()); } onLoadFailed: error => { - if (!isGreeterMode) { - const msg = String(error || ""); - if (!_isMissingPluginSettingsError(error)) - log.warn("Failed to load plugin_settings.json. Error:", msg); - _resetPluginSettings(); - } + const msg = String(error || ""); + if (!_isMissingPluginSettingsError(error)) + log.warn("Failed to load plugin_settings.json. Error:", msg); + _resetPluginSettings(); } } diff --git a/quickshell/Common/Theme.qml b/quickshell/Common/Theme.qml index 6fad37bd9..c8e91b22e 100644 --- a/quickshell/Common/Theme.qml +++ b/quickshell/Common/Theme.qml @@ -8,7 +8,6 @@ import Quickshell.Io import qs.Common import qs.DankCommon.Common as DankCommon import qs.Services -import qs.Modules.Greetd import "StockThemes.js" as StockThemes Singleton { @@ -143,13 +142,11 @@ Singleton { Component.onCompleted: { 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) => { matugenAvailable = (code === 0) && !envDisableMatugen; - const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode); - if (!matugenAvailable || isGreeterMode) { + if (!matugenAvailable) { return; } @@ -428,13 +425,6 @@ Singleton { DMSService.sendRequest("theme.auto.trigger", {}); } - function applyGreeterTheme(themeName) { - switchTheme(themeName, false, false); - if (themeName === dynamic && dynamicColorsFileView.path) { - dynamicColorsFileView.reload(); - } - } - function getMatugenColor(path, fallback) { const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark"; let cur = matugenColors && matugenColors.colors && matugenColors.colors[colorMode]; @@ -1222,26 +1212,11 @@ Singleton { return presetMap[SettingsData.modalAnimationSpeed] ?? 150; } - property real cornerRadius: { - if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") { - return GreetdSettings.cornerRadius; - } - return typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12; - } + property real cornerRadius: typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12 - property string fontFamily: { - 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 fontFamily: typeof SettingsData !== "undefined" ? resolvedFontFamily(SettingsData.fontFamily) : DankCommon.Fonts.sans - property string monoFontFamily: { - if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") { - return resolvedMonoFontFamily(GreetdSettings.monoFontFamily); - } - return typeof SettingsData !== "undefined" ? resolvedMonoFontFamily(SettingsData.monoFontFamily) : DankCommon.Fonts.mono; - } + property string monoFontFamily: typeof SettingsData !== "undefined" ? resolvedMonoFontFamily(SettingsData.monoFontFamily) : DankCommon.Fonts.mono function resolvedFontFamily(family) { if (family === defaultFontFamily) @@ -1255,19 +1230,9 @@ Singleton { return family; } - property int fontWeight: { - if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") { - return GreetdSettings.fontWeight; - } - return typeof SettingsData !== "undefined" ? SettingsData.fontWeight : Font.Normal; - } + property int fontWeight: typeof SettingsData !== "undefined" ? SettingsData.fontWeight : Font.Normal - property real fontScale: { - if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") { - return GreetdSettings.fontScale; - } - return typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0; - } + property real fontScale: typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0 property real spacingXXS: 2 property real spacingXS: 4 @@ -1327,15 +1292,12 @@ Singleton { currentThemeCategory = "generic"; } } - const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode); - if (savePrefs && typeof SettingsData !== "undefined" && !isGreeterMode) { + if (savePrefs && typeof SettingsData !== "undefined") { SettingsData.set("currentThemeCategory", currentThemeCategory); SettingsData.set("currentThemeName", currentTheme); } - if (!isGreeterMode) { - generateSystemThemesFromCurrentTheme(); - } + generateSystemThemesFromCurrentTheme(); } function setLightMode(light, savePrefs = true, enableTransition = false) { @@ -1347,18 +1309,15 @@ Singleton { return; } - const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode); - if (savePrefs && typeof SessionData !== "undefined" && !isGreeterMode) { + if (savePrefs && typeof SessionData !== "undefined") { SessionData.setLightMode(light); } - if (!isGreeterMode) { - PortalService.setLightMode(light); - if (typeof SettingsData !== "undefined") { - SettingsData.updateCosmicThemeMode(light); - } - generateSystemThemesFromCurrentTheme(); + PortalService.setLightMode(light); + if (typeof SettingsData !== "undefined") { + SettingsData.updateCosmicThemeMode(light); } + generateSystemThemesFromCurrentTheme(); } function toggleLightMode(savePrefs = true) { @@ -1412,8 +1371,7 @@ Singleton { if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) { const defaults = themeData.variants.defaults || {}; const modeDefaults = defaults[colorMode] || defaults.dark || {}; - const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode; - const stored = isGreeterMode ? (GreetdSettings.registryThemeVariants[themeId]?.[colorMode] || modeDefaults) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults, colorMode) : modeDefaults); + const stored = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults, colorMode) : modeDefaults; var flavorId = stored.flavor || modeDefaults.flavor || ""; const accentId = stored.accent || modeDefaults.accent || ""; var flavor = findVariant(themeData.variants.flavors, flavorId); @@ -1439,8 +1397,7 @@ Singleton { } if (themeData.variants.options && themeData.variants.options.length > 0) { - const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode; - 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 selectedVariantId = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, themeData.variants.default) : themeData.variants.default; const variant = findVariant(themeData.variants.options, selectedVariantId); if (variant) { const variantColors = variant[colorMode] || variant.dark || variant.light || {}; @@ -1800,8 +1757,7 @@ Singleton { } function generateSystemThemesFromCurrentTheme() { - const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode); - if (!matugenAvailable || isGreeterMode) + if (!matugenAvailable) return; _lastGenerateMs = Date.now(); @@ -1842,9 +1798,8 @@ Singleton { const defaults = customThemeRawData.variants.defaults || {}; const darkDefaults = defaults.dark || {}; const lightDefaults = defaults.light || defaults.dark || {}; - const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode; - const storedDark = isGreeterMode ? (GreetdSettings.registryThemeVariants[themeId]?.dark || darkDefaults) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults); - const storedLight = isGreeterMode ? (GreetdSettings.registryThemeVariants[themeId]?.light || lightDefaults) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults); + const storedDark = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults; + const storedLight = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults; const darkFlavorId = storedDark.flavor || darkDefaults.flavor || ""; const lightFlavorId = storedLight.flavor || lightDefaults.flavor || ""; const darkAccentId = storedDark.accent || darkDefaults.accent || ""; @@ -1864,8 +1819,7 @@ Singleton { lightTheme = mergeColors(lightTheme, lightAccent[lightFlavor.id] || {}); } } else if (customThemeRawData.variants.options) { - const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode; - 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 selectedVariantId = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, customThemeRawData.variants.default) : customThemeRawData.variants.default; const variant = findVariant(customThemeRawData.variants.options, selectedVariantId); if (variant) { 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 { id: dynamicColorsFileView - path: { - if (SessionData.isGreeterMode) - return root.greeterColorsBaseDir ? (root.greeterColorsBaseDir + "/colors.json") : ""; - return stateDir + "/dms-colors.json"; - } + path: stateDir + "/dms-colors.json" blockLoading: false - watchChanges: !SessionData.isGreeterMode + watchChanges: true function parseAndLoadColors() { try { @@ -2274,9 +2207,6 @@ Singleton { if (currentTheme !== dynamic) return; - if (SessionData.isGreeterMode) - return; - if (workerRunning) { colorsReloadRetry.restart(); return; diff --git a/quickshell/Common/settings/Processes.qml b/quickshell/Common/settings/Processes.qml index bdc9fd9ec..ca1a2a6e6 100644 --- a/quickshell/Common/settings/Processes.qml +++ b/quickshell/Common/settings/Processes.qml @@ -13,14 +13,14 @@ Singleton { property var settingsRoot: null onSettingsRootChanged: { - if (settingsRoot && !settingsRoot.isGreeterMode) + if (settingsRoot) consumeGreeterAutoLoginPendingSync(); } readonly property string greeterAutoLoginPendingSyncPath: (Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter") + "/.local/state/auto-login-sync-pending" function consumeGreeterAutoLoginPendingSync() { - if (!settingsRoot || settingsRoot.isGreeterMode) + if (!settingsRoot) return; greeterAutoLoginPendingCheckProcess.running = true; } @@ -287,7 +287,7 @@ Singleton { property string authApplyTerminalFallbackStderr: "" function scheduleAuthApply() { - if (!settingsRoot || settingsRoot.isGreeterMode) + if (!settingsRoot) return; authApplyQueued = true; @@ -300,7 +300,7 @@ Singleton { } function beginAuthApply() { - if (!authApplyQueued || authApplyRunning || !settingsRoot || settingsRoot.isGreeterMode) + if (!authApplyQueued || authApplyRunning || !settingsRoot) return; authApplyQueued = false; @@ -339,7 +339,7 @@ Singleton { property string greeterAutoLoginSyncStderr: "" function scheduleGreeterAutoLoginSync() { - if (!settingsRoot || settingsRoot.isGreeterMode) + if (!settingsRoot) return; greeterAutoLoginSyncQueued = true; @@ -352,7 +352,7 @@ Singleton { } function beginGreeterAutoLoginSync() { - if (!greeterAutoLoginSyncQueued || greeterAutoLoginSyncRunning || !settingsRoot || settingsRoot.isGreeterMode) + if (!greeterAutoLoginSyncQueued || greeterAutoLoginSyncRunning || !settingsRoot) return; greeterAutoLoginSyncQueued = false; diff --git a/quickshell/DMSGreeter.qml b/quickshell/DMSGreeter.qml deleted file mode 100644 index f3068f5dd..000000000 --- a/quickshell/DMSGreeter.qml +++ /dev/null @@ -1,9 +0,0 @@ -import QtQuick -import Quickshell -import qs.Modules.Greetd - -Scope { - id: root - - GreeterSurface {} -} diff --git a/quickshell/Modules/BlurredWallpaperBackground.qml b/quickshell/Modules/BlurredWallpaperBackground.qml index 977bbf1f3..8e4eda085 100644 --- a/quickshell/Modules/BlurredWallpaperBackground.qml +++ b/quickshell/Modules/BlurredWallpaperBackground.qml @@ -7,12 +7,7 @@ import qs.Services Variants { readonly property var log: Log.scoped("BlurredWallpaperBackground") - model: { - if (SessionData.isGreeterMode) { - return Quickshell.screens; - } - return SettingsData.getFilteredScreens("wallpaper"); - } + model: SettingsData.getFilteredScreens("wallpaper") PanelWindow { id: blurWallpaperWindow diff --git a/quickshell/Modules/BlurredWallpaperLive.qml b/quickshell/Modules/BlurredWallpaperLive.qml index 7365fba77..16753b1ac 100644 --- a/quickshell/Modules/BlurredWallpaperLive.qml +++ b/quickshell/Modules/BlurredWallpaperLive.qml @@ -144,7 +144,7 @@ Item { smooth: true cache: true sourceSize: root.blurTextureSize - fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SessionData.getMonitorWallpaperFillMode(root.screenName)) + fillMode: root.getFillMode(SessionData.getMonitorWallpaperFillMode(root.screenName)) onStatusChanged: { if (status === Image.Error) { @@ -166,7 +166,7 @@ Item { smooth: true cache: true sourceSize: root.blurTextureSize - fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SessionData.getMonitorWallpaperFillMode(root.screenName)) + fillMode: root.getFillMode(SessionData.getMonitorWallpaperFillMode(root.screenName)) onStatusChanged: { if (status === Image.Error) { diff --git a/quickshell/Modules/DankBar/Widgets/KeyboardLayoutName.qml b/quickshell/Modules/DankBar/Widgets/KeyboardLayoutName.qml index 9f7cb2f48..6590e0199 100644 --- a/quickshell/Modules/DankBar/Widgets/KeyboardLayoutName.qml +++ b/quickshell/Modules/DankBar/Widgets/KeyboardLayoutName.qml @@ -6,7 +6,7 @@ import qs.Common import qs.Modules.Plugins import qs.Services import qs.Widgets -import "../../../Common/LayoutCodes.js" as LayoutCodes +import "../../../DankCommon/Common/LayoutCodes.js" as LayoutCodes BasePill { id: root diff --git a/quickshell/Modules/Greetd/GreetdEnv.js b/quickshell/Modules/Greetd/GreetdEnv.js deleted file mode 100644 index fcce181e5..000000000 --- a/quickshell/Modules/Greetd/GreetdEnv.js +++ /dev/null @@ -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; -} diff --git a/quickshell/Modules/Greetd/GreetdMemory.qml b/quickshell/Modules/Greetd/GreetdMemory.qml deleted file mode 100644 index 54de73bc7..000000000 --- a/quickshell/Modules/Greetd/GreetdMemory.qml +++ /dev/null @@ -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: {} - } -} diff --git a/quickshell/Modules/Greetd/GreetdSettings.qml b/quickshell/Modules/Greetd/GreetdSettings.qml deleted file mode 100644 index 9b83fc413..000000000 --- a/quickshell/Modules/Greetd/GreetdSettings.qml +++ /dev/null @@ -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(""); - } - } -} diff --git a/quickshell/Modules/Greetd/GreeterContent.qml b/quickshell/Modules/Greetd/GreeterContent.qml deleted file mode 100644 index 81566fd51..000000000 --- a/quickshell/Modules/Greetd/GreeterContent.qml +++ /dev/null @@ -1,2135 +0,0 @@ -import QtQuick -import QtQuick.Effects -import QtQuick.Layouts -import Qt.labs.folderlistmodel -import Quickshell -import Quickshell.Hyprland -import Quickshell.Io -import Quickshell.Services.Greetd -import qs.Common -import qs.Services -import qs.Widgets -import qs.Modules.Lock -import "../../Common/LayoutCodes.js" as LayoutCodes - -Item { - id: root - - function encodeFileUrl(path) { - if (!path) - return ""; - return "file://" + path.split('/').map(s => encodeURIComponent(s)).join('/'); - } - - function desktopIdFromPath(path) { - if (!path) - return ""; - const parts = path.split("/"); - const id = parts.length > 0 ? parts[parts.length - 1] : path; - return id || ""; - } - - readonly property string xdgDataDirs: Quickshell.env("XDG_DATA_DIRS") - property string screenName: "" - property string hyprlandCurrentLayout: "" - property string hyprlandKeyboard: "" - property int hyprlandLayoutCount: 0 - property bool isPrimaryScreen: !Quickshell.screens?.length || screenName === Quickshell.screens[0]?.name - - signal launchRequested - - property bool weatherInitialized: false - property bool awaitingExternalAuth: false - property bool pendingPasswordResponse: false - property bool passwordSubmitRequested: false - property bool cancelingExternalAuthForPassword: false - property int defaultAuthTimeoutMs: 10000 - property int externalAuthTimeoutMs: 30000 - property int memoryFlushDelayMs: 120 - property string pendingLaunchCommand: "" - property var pendingLaunchEnv: [] - property int passwordFailureCount: 0 - property int passwordAttemptLimitHint: 0 - property string authFeedbackMessage: "" - property string authSuccessMessage: "" - property string greetdPamText: "" - property string systemAuthPamText: "" - property string commonAuthPamText: "" - property string passwordAuthPamText: "" - property string systemLoginPamText: "" - property string systemLocalLoginPamText: "" - property string commonAuthPcPamText: "" - property string loginPamText: "" - property string faillockConfigText: "" - property bool greeterWallpaperOverrideExists: false - property string externalAuthAutoStartedForUser: "" - property int passwordSessionTransitionRetryCount: 0 - property int maxPasswordSessionTransitionRetries: 2 - property bool fprintdProbeComplete: false - property bool fprintdHasDevice: false - property bool autoLoginOnSuccess: false - readonly property bool greeterPamStackHasFprint: greeterPamStackHasModule("pam_fprintd") - // Falls back to PAM-only detection until the fprintd D-Bus probe completes. - readonly property bool greeterPamHasFprint: greeterPamStackHasFprint && (!fprintdProbeComplete || fprintdHasDevice) - readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f") - readonly property bool greeterExternalAuthAvailable: (greeterPamHasFprint && GreetdSettings.greeterEnableFprint) || (greeterPamHasU2f && GreetdSettings.greeterEnableU2f) - readonly property bool greeterPamHasExternalAuth: greeterPamHasFprint || greeterPamHasU2f - readonly property bool externalAuthInProgress: awaitingExternalAuth || (Greetd.state !== GreetdState.Inactive && passwordSubmitRequested && greeterPamHasExternalAuth && !pendingPasswordResponse) - readonly property string externalAuthStatusMessage: { - if (!externalAuthInProgress) - return ""; - if (greeterPamHasFprint && greeterPamHasU2f) - return I18n.tr("Awaiting fingerprint or security key authentication"); - if (greeterPamHasFprint) - return I18n.tr("Awaiting fingerprint authentication"); - return I18n.tr("Awaiting security key authentication"); - } - readonly property string authDisplayMessage: authFeedbackMessage || authSuccessMessage || externalAuthStatusMessage - readonly property bool autoLoginAvailable: GreetdSettings.rememberLastUser && GreetdSettings.rememberLastSession - readonly property bool multipleUsersAvailable: GreeterUsersService.loaded && GreeterUsersService.users.length > 1 - // Single-user systems get the picker too when auto-login is available, so the - // auto-login toggle lives inside the dropdown instead of floating on its own. - readonly property bool pickerAvailable: multipleUsersAvailable || (GreeterUsersService.loaded && GreeterUsersService.users.length === 1 && autoLoginAvailable) - readonly property bool showUserPicker: pickerAvailable && !GreeterState.showPasswordInput && !manualUsernameEntry - readonly property bool showAccountSwitchLink: pickerAvailable && manualUsernameEntry && !GreeterState.showPasswordInput && !GreeterState.unlocking - readonly property int userPickerMaxHeight: Math.min(400, Math.max(120, height * 0.35)) - property bool userListOpen: false - property bool manualUsernameEntry: false - property bool skipAutoSelectUser: false - property string pickerThemeUsername: "" - - function initWeatherService() { - if (weatherInitialized) - return; - if (!GreetdSettings.settingsLoaded) - return; - if (!GreetdSettings.weatherEnabled) - return; - weatherInitialized = true; - WeatherService.addRef(); - WeatherService.forceRefresh(); - } - - function stripPamComment(line) { - if (!line) - return ""; - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) - return ""; - const hashIdx = trimmed.indexOf("#"); - if (hashIdx >= 0) - return trimmed.substring(0, hashIdx).trim(); - return trimmed; - } - - function pamModuleEnabled(pamText, moduleName) { - if (!pamText || !moduleName) - return false; - const lines = pamText.split(/\r?\n/); - for (let i = 0; i < lines.length; i++) { - const line = stripPamComment(lines[i]); - if (!line) - continue; - if (line.includes(moduleName)) - return true; - } - return false; - } - - function pamTextIncludesFile(pamText, filename) { - if (!pamText || !filename) - return false; - const lines = pamText.split(/\r?\n/); - for (let i = 0; i < lines.length; i++) { - const line = stripPamComment(lines[i]); - if (!line) - continue; - if (line.includes(filename) && (line.includes("include") || line.includes("substack") || line.startsWith("@include"))) - return true; - } - return false; - } - - function greeterPamStackHasModule(moduleName) { - if (pamModuleEnabled(greetdPamText, moduleName)) - return true; - const includedPamStacks = [["system-auth", systemAuthPamText], ["common-auth", commonAuthPamText], ["password-auth", passwordAuthPamText], ["system-login", systemLoginPamText], ["system-local-login", systemLocalLoginPamText], ["common-auth-pc", commonAuthPcPamText], ["login", loginPamText]]; - for (let i = 0; i < includedPamStacks.length; i++) { - const stack = includedPamStacks[i]; - if (pamTextIncludesFile(greetdPamText, stack[0]) && pamModuleEnabled(stack[1], moduleName)) - return true; - } - return false; - } - - function usesPamLockoutPolicy(pamText) { - if (!pamText) - return false; - const lines = pamText.split(/\r?\n/); - for (let i = 0; i < lines.length; i++) { - const line = stripPamComment(lines[i]); - if (!line) - continue; - if (line.includes("pam_faillock.so") || line.includes("pam_tally2.so") || line.includes("pam_tally.so")) - return true; - } - return false; - } - - function parsePamLineDenyValue(pamText) { - if (!pamText) - return -1; - const lines = pamText.split(/\r?\n/); - for (let i = 0; i < lines.length; i++) { - const line = stripPamComment(lines[i]); - if (!line) - continue; - if (!line.includes("pam_faillock.so") && !line.includes("pam_tally2.so") && !line.includes("pam_tally.so")) - continue; - const denyMatch = line.match(/\bdeny\s*=\s*(\d+)\b/i); - if (!denyMatch) - continue; - const parsed = parseInt(denyMatch[1], 10); - if (!isNaN(parsed)) - return parsed; - } - return -1; - } - - function parseFaillockDenyValue(configText) { - if (!configText) - return -1; - const lines = configText.split(/\r?\n/); - for (let i = 0; i < lines.length; i++) { - const line = stripPamComment(lines[i]); - if (!line) - continue; - const denyMatch = line.match(/^deny\s*=\s*(\d+)\s*$/i); - if (!denyMatch) - continue; - const parsed = parseInt(denyMatch[1], 10); - if (!isNaN(parsed)) - return parsed; - } - return -1; - } - - function refreshPasswordAttemptPolicyHint() { - const pamSources = [greetdPamText, systemAuthPamText, commonAuthPamText, passwordAuthPamText, systemLoginPamText, systemLocalLoginPamText, commonAuthPcPamText, loginPamText]; - let lockoutConfigured = false; - let denyFromPam = -1; - for (let i = 0; i < pamSources.length; i++) { - const source = pamSources[i]; - if (!source) - continue; - if (usesPamLockoutPolicy(source)) - lockoutConfigured = true; - const denyValue = parsePamLineDenyValue(source); - if (denyValue >= 0 && (denyFromPam < 0 || denyValue < denyFromPam)) - denyFromPam = denyValue; - } - - if (!lockoutConfigured) { - passwordAttemptLimitHint = 0; - return; - } - - const denyFromConfig = parseFaillockDenyValue(faillockConfigText); - if (denyFromConfig >= 0) { - passwordAttemptLimitHint = denyFromConfig; - return; - } - - if (denyFromPam >= 0) { - passwordAttemptLimitHint = denyFromPam; - return; - } - - // pam_faillock default deny value when no explicit config is set. - passwordAttemptLimitHint = 3; - } - - function isLikelyLockoutMessage(message) { - const lower = (message || "").toLowerCase(); - return lower.includes("account is locked") || lower.includes("too many") || lower.includes("maximum number of"); - } - - function currentAuthMessage() { - if (GreeterState.pamState === "error") - return I18n.tr("Authentication error - try again"); - if (GreeterState.pamState === "max") - return I18n.tr("Too many failed attempts - account may be locked"); - if (GreeterState.pamState === "fail") { - if (passwordAttemptLimitHint > 0) { - const attempt = Math.max(1, Math.min(passwordFailureCount, passwordAttemptLimitHint)); - const remaining = Math.max(passwordAttemptLimitHint - attempt, 0); - if (remaining > 0) { - return I18n.tr("Authentication failed - attempt %1 of %2").arg(attempt).arg(passwordAttemptLimitHint); - } - return I18n.tr("Authentication failed - lockout can occur"); - } - return I18n.tr("Authentication failed - try again"); - } - return ""; - } - - function clearAuthFeedback() { - GreeterState.pamState = ""; - authFeedbackMessage = ""; - authSuccessMessage = ""; - } - - function resetPasswordSessionTransition(clearSubmitRequest) { - cancelingExternalAuthForPassword = false; - passwordSessionTransitionRetryCount = 0; - if (clearSubmitRequest) - passwordSubmitRequested = false; - } - - Connections { - target: GreetdSettings - function onSettingsLoadedChanged() { - if (GreetdSettings.settingsLoaded) { - initWeatherService(); - if (isPrimaryScreen) { - applyLastSuccessfulUser(); - finalizeSessionSelection(); - } - } - } - - function onRememberLastUserChanged() { - if (!isPrimaryScreen) - return; - if (!GreetdSettings.rememberLastUser && GreetdMemory.lastSuccessfulUser) { - GreetdMemory.setLastSuccessfulUser(""); - } - applyLastSuccessfulUser(); - } - - function onRememberLastSessionChanged() { - if (!isPrimaryScreen) - return; - if (!GreetdSettings.rememberLastSession && (GreetdMemory.lastSessionId || GreetdMemory.lastSessionDesktopId || GreetdMemory.lastSessionExec)) { - GreetdMemory.setLastSession("", ""); - } - finalizeSessionSelection(); - } - } - - FileView { - id: greetdPamWatcher - path: "/etc/pam.d/greetd" - printErrors: false - onLoaded: { - root.greetdPamText = text(); - root.refreshPasswordAttemptPolicyHint(); - root.maybeAutoStartExternalAuth(); - } - onLoadFailed: { - root.greetdPamText = ""; - root.refreshPasswordAttemptPolicyHint(); - } - } - - FileView { - id: systemAuthPamWatcher - path: "/etc/pam.d/system-auth" - printErrors: false - onLoaded: { - root.systemAuthPamText = text(); - root.refreshPasswordAttemptPolicyHint(); - root.maybeAutoStartExternalAuth(); - } - onLoadFailed: { - root.systemAuthPamText = ""; - root.refreshPasswordAttemptPolicyHint(); - } - } - - FileView { - id: commonAuthPamWatcher - path: "/etc/pam.d/common-auth" - printErrors: false - onLoaded: { - root.commonAuthPamText = text(); - root.refreshPasswordAttemptPolicyHint(); - root.maybeAutoStartExternalAuth(); - } - onLoadFailed: { - root.commonAuthPamText = ""; - root.refreshPasswordAttemptPolicyHint(); - } - } - - FileView { - id: passwordAuthPamWatcher - path: "/etc/pam.d/password-auth" - printErrors: false - onLoaded: { - root.passwordAuthPamText = text(); - root.refreshPasswordAttemptPolicyHint(); - root.maybeAutoStartExternalAuth(); - } - onLoadFailed: { - root.passwordAuthPamText = ""; - root.refreshPasswordAttemptPolicyHint(); - } - } - - FileView { - id: systemLoginPamWatcher - path: "/etc/pam.d/system-login" - printErrors: false - onLoaded: { - root.systemLoginPamText = text(); - root.refreshPasswordAttemptPolicyHint(); - root.maybeAutoStartExternalAuth(); - } - onLoadFailed: { - root.systemLoginPamText = ""; - root.refreshPasswordAttemptPolicyHint(); - } - } - - FileView { - id: systemLocalLoginPamWatcher - path: "/etc/pam.d/system-local-login" - printErrors: false - onLoaded: { - root.systemLocalLoginPamText = text(); - root.refreshPasswordAttemptPolicyHint(); - root.maybeAutoStartExternalAuth(); - } - onLoadFailed: { - root.systemLocalLoginPamText = ""; - root.refreshPasswordAttemptPolicyHint(); - } - } - - FileView { - id: commonAuthPcPamWatcher - path: "/etc/pam.d/common-auth-pc" - printErrors: false - onLoaded: { - root.commonAuthPcPamText = text(); - root.refreshPasswordAttemptPolicyHint(); - root.maybeAutoStartExternalAuth(); - } - onLoadFailed: { - root.commonAuthPcPamText = ""; - root.refreshPasswordAttemptPolicyHint(); - } - } - - FileView { - id: loginPamWatcher - path: "/etc/pam.d/login" - printErrors: false - onLoaded: { - root.loginPamText = text(); - root.refreshPasswordAttemptPolicyHint(); - root.maybeAutoStartExternalAuth(); - } - onLoadFailed: { - root.loginPamText = ""; - root.refreshPasswordAttemptPolicyHint(); - } - } - - FileView { - id: faillockConfigWatcher - path: "/etc/security/faillock.conf" - printErrors: false - onLoaded: { - root.faillockConfigText = text(); - root.refreshPasswordAttemptPolicyHint(); - } - onLoadFailed: { - root.faillockConfigText = ""; - root.refreshPasswordAttemptPolicyHint(); - } - } - - Component.onCompleted: { - initWeatherService(); - refreshPasswordAttemptPolicyHint(); - - if (isPrimaryScreen) - applyLastSuccessfulUser(); - - if (CompositorService.isHyprland) - updateHyprlandLayout(); - - fprintdDeviceProbe.running = true; - } - - function applyPickerPreviewTheme() { - let previewUser = (pickerThemeUsername || "").trim(); - if (!previewUser && GreetdSettings.rememberLastUser) - previewUser = (GreetdMemory.lastSuccessfulUser || "").trim(); - if (previewUser) - GreeterUserTheme.applyForUser(previewUser); - else - GreeterUserTheme.applyDefault(); - } - - function applyLastSuccessfulUser() { - if (root.skipAutoSelectUser) - return; - if (!GreetdSettings.settingsLoaded || !GreetdSettings.rememberLastUser) - return; - const lastUser = GreetdMemory.lastSuccessfulUser; - if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) { - selectUser(lastUser, true); - } - } - - function enterManualUsernameEntry() { - if (!root.pickerAvailable || GreeterState.showPasswordInput) - return; - root.manualUsernameEntry = true; - root.userListOpen = false; - GreeterState.username = ""; - GreeterState.usernameInput = ""; - GreeterState.selectedUserIndex = -1; - inputField.text = ""; - root.applyPickerPreviewTheme(); - Qt.callLater(() => inputField.forceActiveFocus()); - } - - function returnToUserListFromManualEntry() { - if (!root.pickerAvailable) - return; - root.manualUsernameEntry = false; - root.userListOpen = true; - GreeterState.username = ""; - GreeterState.usernameInput = ""; - inputField.text = ""; - root.applyPickerPreviewTheme(); - } - - function returnToUserPicker() { - if (!root.pickerAvailable || GreeterState.unlocking) - return; - root.manualUsernameEntry = false; - root.skipAutoSelectUser = true; - awaitingExternalAuth = false; - pendingPasswordResponse = false; - passwordSubmitRequested = false; - resetPasswordSessionTransition(true); - authTimeout.interval = defaultAuthTimeoutMs; - authTimeout.stop(); - clearAuthFeedback(); - passwordFailureCount = 0; - externalAuthAutoStartedForUser = ""; - if (Greetd.state !== GreetdState.Inactive) - Greetd.cancelSession(); - const previousUser = GreeterState.username; - GreeterState.reset(); - inputField.text = ""; - PortalService.profileImage = ""; - if (previousUser) - root.pickerThemeUsername = previousUser; - root.applyPickerPreviewTheme(); - root.userListOpen = true; - } - - function selectUser(rawValue, skipDropdownUpdate) { - const user = (rawValue || "").trim(); - if (!user) - return; - root.manualUsernameEntry = false; - root.skipAutoSelectUser = false; - submitUsername(user, skipDropdownUpdate === true); - } - - function submitUsername(rawValue, skipDropdownUpdate) { - const user = (rawValue || "").trim(); - if (!user) - return; - if (GreeterState.username !== user) { - passwordFailureCount = 0; - clearAuthFeedback(); - externalAuthAutoStartedForUser = ""; - } - root.pickerThemeUsername = user; - GreeterState.username = user; - GreeterState.usernameInput = user; - GreeterState.showPasswordInput = true; - if (!skipDropdownUpdate && typeof GreeterUsersService !== "undefined") { - const idx = GreeterUsersService.usernames.indexOf(user); - GreeterState.selectedUserIndex = idx; - } - root.userListOpen = false; - PortalService.getGreeterUserProfileImage(user); - GreeterState.passwordBuffer = ""; - pendingPasswordResponse = false; - resetPasswordSessionTransition(true); - maybeAutoStartExternalAuth(); - } - - function submitBufferedPassword() { - pendingPasswordResponse = false; - resetPasswordSessionTransition(true); - awaitingExternalAuth = false; - authTimeout.interval = defaultAuthTimeoutMs; - authTimeout.restart(); - // Some PAM stacks expect an explicit empty response to advance U2F/fprint or fail normally. - Greetd.respond(GreeterState.passwordBuffer || ""); - GreeterState.passwordBuffer = ""; - inputField.text = ""; - return true; - } - - function requestPasswordSessionTransition() { - const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0; - if (!passwordSubmitRequested && !hasPasswordBuffer) - return; - if (cancelingExternalAuthForPassword) - return; - if (passwordSessionTransitionRetryCount >= maxPasswordSessionTransitionRetries) { - pendingPasswordResponse = false; - awaitingExternalAuth = false; - authTimeout.interval = defaultAuthTimeoutMs; - authTimeout.stop(); - resetPasswordSessionTransition(true); - GreeterState.pamState = "error"; - authFeedbackMessage = currentAuthMessage(); - placeholderDelay.restart(); - Greetd.cancelSession(); - return; - } - cancelingExternalAuthForPassword = true; - passwordSessionTransitionRetryCount = passwordSessionTransitionRetryCount + 1; - awaitingExternalAuth = false; - pendingPasswordResponse = false; - authTimeout.interval = defaultAuthTimeoutMs; - authTimeout.stop(); - Greetd.cancelSession(); - } - - function startAuthSession(submitPassword) { - submitPassword = submitPassword === true; - if (!GreeterState.showPasswordInput || !GreeterState.username) - return; - if (GreeterState.unlocking) - return; - const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0; - if (Greetd.state !== GreetdState.Inactive) { - if (pendingPasswordResponse && submitPassword) - submitBufferedPassword(); - else if (submitPassword) - passwordSubmitRequested = true; - return; - } - if (cancelingExternalAuthForPassword) { - if (submitPassword) - passwordSubmitRequested = true; - return; - } - if (!submitPassword && !hasPasswordBuffer && !root.greeterExternalAuthAvailable) - return; - pendingPasswordResponse = false; - passwordSubmitRequested = submitPassword; - awaitingExternalAuth = !submitPassword && !hasPasswordBuffer && root.greeterExternalAuthAvailable; - // Let the effective PAM stack finish external authentication. - const waitingOnPamExternalBeforePassword = submitPassword && root.greeterPamHasExternalAuth; - authTimeout.interval = (awaitingExternalAuth || waitingOnPamExternalBeforePassword) ? externalAuthTimeoutMs : defaultAuthTimeoutMs; - authTimeout.restart(); - Greetd.createSession(GreeterState.username); - } - - function maybeAutoStartExternalAuth() { - if (!GreeterState.showPasswordInput || !GreeterState.username) - return; - if (!root.greeterExternalAuthAvailable) - return; - if (GreeterState.unlocking || Greetd.state !== GreetdState.Inactive) - return; - if (passwordSubmitRequested || cancelingExternalAuthForPassword) - return; - if (GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0) - return; - if (externalAuthAutoStartedForUser === GreeterState.username) - return; - - externalAuthAutoStartedForUser = GreeterState.username; - startAuthSession(false); - } - - function isExternalAuthPrompt(message, responseRequired) { - // Non-response PAM messages commonly represent waiting states (fprint/U2F/token touch). - return !responseRequired; - } - - Component.onDestruction: { - if (weatherInitialized) - WeatherService.removeRef(); - } - - function updateHyprlandLayout() { - if (CompositorService.isHyprland) { - hyprlandLayoutProcess.running = true; - } - } - - Process { - id: greeterAutoLoginPendingProcess - command: ["sh", "-c", "mkdir -p $(dirname " + JSON.stringify((Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter") + "/.local/state/auto-login-sync-pending") + ") && touch " + JSON.stringify((Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter") + "/.local/state/auto-login-sync-pending")] - running: false - } - - Process { - id: hyprlandLayoutProcess - running: false - command: ["hyprctl", "-j", "devices"] - stdout: StdioCollector { - onStreamFinished: { - try { - const data = JSON.parse(text); - const mainKeyboard = data.keyboards.find(kb => kb.main === true); - if (!mainKeyboard) { - hyprlandCurrentLayout = ""; - hyprlandLayoutCount = 0; - return; - } - hyprlandKeyboard = mainKeyboard.name; - if (mainKeyboard.active_keymap) { - hyprlandCurrentLayout = LayoutCodes.layoutCode(mainKeyboard.active_keymap); - } else { - hyprlandCurrentLayout = ""; - } - hyprlandLayoutCount = mainKeyboard.layout ? mainKeyboard.layout.split(",").length : 0; - } catch (e) { - hyprlandCurrentLayout = ""; - hyprlandLayoutCount = 0; - } - } - } - } - - // Probe fprintd D-Bus for physically enrolled scanners to eliminate PAM stack false-positives. - Process { - id: fprintdDeviceProbe - running: false - // sh wrapper: emits PROBE_UNAVAILABLE if gdbus is absent or fprintd unreachable, - // keeping the PAM-only fallback active in those cases. - command: ["sh", "-c", "command -v gdbus >/dev/null 2>&1 || { echo PROBE_UNAVAILABLE; exit 0; }; " + "gdbus call --system " + "--dest net.reactivated.Fprint " + "--object-path /net/reactivated/Fprint/Manager " + "--method net.reactivated.Fprint.Manager.GetDevices 2>/dev/null " + "|| echo PROBE_UNAVAILABLE"] - stdout: StdioCollector { - onStreamFinished: { - if (text.includes("PROBE_UNAVAILABLE")) - return; // PAM-only fallback stays active - root.fprintdHasDevice = text.includes("objectpath"); - root.fprintdProbeComplete = true; - root.maybeAutoStartExternalAuth(); - } - } - onExited: function (exitCode, exitStatus) { - if (!root.fprintdProbeComplete) - root.maybeAutoStartExternalAuth(); // PAM-only fallback stays active - } - } - - Connections { - target: CompositorService.isHyprland ? Hyprland : null - enabled: CompositorService.isHyprland - - function onRawEvent(event) { - if (event.name === "activelayout") - updateHyprlandLayout(); - } - } - - Connections { - target: GreetdMemory - enabled: isPrimaryScreen - function onLastSuccessfulUserChanged() { - applyLastSuccessfulUser(); - } - function onMemoryReadyChanged() { - finalizeSessionSelection(); - } - } - - Connections { - target: GreeterUsersService - function onLoadedChanged() { - if (GreeterUsersService.loaded && isPrimaryScreen) - applyPickerPreviewTheme(); - } - function onSyncedThemePathsChanged() { - if (!isPrimaryScreen) - return; - if (GreeterState.username) - GreeterUserTheme.applyForUser(GreeterState.username); - else if (root.showUserPicker || root.userListOpen) - applyPickerPreviewTheme(); - } - } - - Connections { - target: GreeterState - function onUsernameChanged() { - if (GreeterState.username) { - root.pickerThemeUsername = GreeterState.username; - GreeterUserTheme.applyForUser(GreeterState.username); - PortalService.getGreeterUserProfileImage(GreeterState.username); - } else if (root.showUserPicker || root.userListOpen) { - applyPickerPreviewTheme(); - } - } - function onShowPasswordInputChanged() { - if (GreeterState.showPasswordInput) - root.userListOpen = false; - } - } - - onShowUserPickerChanged: { - if (showUserPicker && !GreeterState.username) - applyPickerPreviewTheme(); - if (!showUserPicker) - userListOpen = false; - } - - FileView { - id: greeterWallpaperOverrideFile - path: GreetdSettings.greeterWallpaperOverridePath - printErrors: false - watchChanges: true - onLoaded: root.greeterWallpaperOverrideExists = true - onLoadFailed: root.greeterWallpaperOverrideExists = false - } - - Connections { - target: GreetdSettings - function onGreeterWallpaperOverridePathChanged() { - if (!GreetdSettings.greeterWallpaperOverridePath) { - root.greeterWallpaperOverrideExists = false; - return; - } - greeterWallpaperOverrideFile.reload(); - } - function onGreeterWallpaperPathChanged() { - if (!GreetdSettings.greeterWallpaperPath) { - root.greeterWallpaperOverrideExists = false; - return; - } - greeterWallpaperOverrideFile.reload(); - } - } - - Rectangle { - anchors.fill: parent - color: GreetdSettings.effectiveWallpaperBackgroundColor - } - - DankBackdrop { - anchors.fill: parent - screenName: root.screenName - visible: { - if (GreetdSettings.greeterWallpaperPath !== "" && root.greeterWallpaperOverrideExists) - return false; - var _ = SessionData.perMonitorWallpaper; - var __ = SessionData.monitorWallpapers; - var currentWallpaper = SessionData.getMonitorWallpaper(screenName); - return !currentWallpaper || currentWallpaper === "" || (currentWallpaper && currentWallpaper.startsWith("#")); - } - } - - Image { - id: wallpaperBackground - - anchors.fill: parent - source: { - if (GreetdSettings.greeterWallpaperPath !== "" && root.greeterWallpaperOverrideExists) - return encodeFileUrl(GreetdSettings.greeterWallpaperOverridePath); - var _ = SessionData.perMonitorWallpaper; - var __ = SessionData.monitorWallpapers; - var currentWallpaper = SessionData.getMonitorWallpaper(screenName); - return (currentWallpaper && !currentWallpaper.startsWith("#")) ? encodeFileUrl(currentWallpaper) : ""; - } - fillMode: Theme.getFillMode(GreetdSettings.getEffectiveWallpaperFillMode()) - smooth: true - asynchronous: false - cache: true - visible: source !== "" - layer.enabled: true - - layer.effect: MultiEffect { - autoPaddingEnabled: false - blurEnabled: true - blur: 0.8 - blurMax: 32 - blurMultiplier: 1 - } - - Behavior on opacity { - NumberAnimation { - duration: Theme.mediumDuration - easing.type: Theme.standardEasing - } - } - } - - Rectangle { - anchors.fill: parent - color: "black" - opacity: 0.4 - } - - SystemClock { - id: systemClock - precision: SystemClock.Seconds - } - - Rectangle { - anchors.fill: parent - color: "transparent" - - MouseArea { - anchors.fill: parent - enabled: root.userListOpen - visible: root.userListOpen - onClicked: root.userListOpen = false - } - - Column { - id: greeterMainColumn - - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingM - width: 380 - - Item { - id: clockContainer - - width: parent.width - height: clockText.implicitHeight - - Row { - id: clockText - - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - spacing: 0 - - property string fullTimeStr: { - const format = GreetdSettings.getEffectiveTimeFormat(); - return systemClock.date.toLocaleTimeString(I18n.locale(), format); - } - property var timeParts: fullTimeStr.split(':') - property string hours: timeParts[0] || "" - property string minutes: timeParts[1] || "" - property string secondsWithAmPm: timeParts.length > 2 ? timeParts[2] : "" - property string seconds: secondsWithAmPm.replace(/\s*(AM|PM|am|pm)$/i, '') - property string ampm: { - const match = fullTimeStr.match(/\s*(AM|PM|am|pm)$/i); - return match ? match[0].trim() : ""; - } - property bool hasSeconds: timeParts.length > 2 - - StyledText { - width: 75 - text: clockText.hours.length > 1 ? clockText.hours[0] : "" - font.pixelSize: 120 - font.weight: Font.Light - color: "white" - horizontalAlignment: Text.AlignHCenter - } - - StyledText { - width: 75 - text: clockText.hours.length > 1 ? clockText.hours[1] : clockText.hours.length > 0 ? clockText.hours[0] : "" - font.pixelSize: 120 - font.weight: Font.Light - color: "white" - horizontalAlignment: Text.AlignHCenter - } - - StyledText { - text: ":" - font.pixelSize: 120 - font.weight: Font.Light - color: "white" - } - - StyledText { - width: 75 - text: clockText.minutes.length > 0 ? clockText.minutes[0] : "" - font.pixelSize: 120 - font.weight: Font.Light - color: "white" - horizontalAlignment: Text.AlignHCenter - } - - StyledText { - width: 75 - text: clockText.minutes.length > 1 ? clockText.minutes[1] : "" - font.pixelSize: 120 - font.weight: Font.Light - color: "white" - horizontalAlignment: Text.AlignHCenter - } - - StyledText { - text: clockText.hasSeconds ? ":" : "" - font.pixelSize: 120 - font.weight: Font.Light - color: "white" - visible: clockText.hasSeconds - } - - StyledText { - width: 75 - text: clockText.hasSeconds && clockText.seconds.length > 0 ? clockText.seconds[0] : "" - font.pixelSize: 120 - font.weight: Font.Light - color: "white" - horizontalAlignment: Text.AlignHCenter - visible: clockText.hasSeconds - } - - StyledText { - width: 75 - text: clockText.hasSeconds && clockText.seconds.length > 1 ? clockText.seconds[1] : "" - font.pixelSize: 120 - font.weight: Font.Light - color: "white" - horizontalAlignment: Text.AlignHCenter - visible: clockText.hasSeconds - } - - StyledText { - width: 20 - text: " " - font.pixelSize: 120 - font.weight: Font.Light - color: "white" - visible: clockText.ampm !== "" - } - - StyledText { - text: clockText.ampm - font.pixelSize: 120 - font.weight: Font.Light - color: "white" - visible: clockText.ampm !== "" - } - } - } - - StyledText { - id: dateText - - anchors.horizontalCenter: parent.horizontalCenter - text: systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.getEffectiveLockDateFormat()) - font.pixelSize: Theme.fontSizeXLarge - color: "white" - opacity: 0.9 - } - - ColumnLayout { - id: authColumn - - width: parent.width - spacing: Theme.spacingM - - RowLayout { - spacing: Theme.spacingL - Layout.fillWidth: true - - Item { - Layout.preferredWidth: 60 - Layout.preferredHeight: 60 - visible: GreetdSettings.lockScreenShowProfileImage || root.pickerAvailable - - DankCircularImage { - anchors.fill: parent - imageSource: { - const displayUser = GreeterState.username || root.pickerThemeUsername; - if (displayUser) { - const cachedPath = GreeterUsersService.profileImagePath(displayUser); - if (cachedPath) - return encodeFileUrl(cachedPath); - } - if (PortalService.profileImage === "") - return ""; - if (PortalService.profileImage.startsWith("/")) - return encodeFileUrl(PortalService.profileImage); - return PortalService.profileImage; - } - fallbackIcon: "person" - } - - Rectangle { - anchors.fill: parent - radius: width / 2 - color: "transparent" - border.color: Theme.primary - border.width: (avatarPickerArea.containsMouse || root.userListOpen) && !GreeterState.showPasswordInput ? 2 : 0 - visible: root.pickerAvailable - Behavior on border.width { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing - } - } - } - - // Switch-user affordance: hover scrim over the selected user's avatar. - Rectangle { - anchors.fill: parent - radius: width / 2 - color: Qt.rgba(0, 0, 0, 0.55) - opacity: (root.pickerAvailable && GreeterState.showPasswordInput && avatarPickerArea.containsMouse) ? 1 : 0 - visible: opacity > 0 - - Behavior on opacity { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing - } - } - - DankIcon { - anchors.centerIn: parent - name: "switch_account" - size: 24 - color: "white" - } - } - - MouseArea { - id: avatarPickerArea - - anchors.fill: parent - visible: root.pickerAvailable - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (GreeterState.showPasswordInput) - root.returnToUserPicker(); - else if (root.manualUsernameEntry) - root.returnToUserListFromManualEntry(); - else - root.userListOpen = !root.userListOpen; - } - } - } - - Rectangle { - property bool showPassword: false - - Layout.fillWidth: true - Layout.preferredHeight: root.showUserPicker && root.userListOpen ? Math.max(60, userPicker.implicitHeight + Theme.spacingM * 2) : 60 - - clip: true - radius: Theme.cornerRadius - color: Theme.withAlpha(Theme.surfaceContainer, 0.9) - border.color: inputField.activeFocus ? Theme.primary : Qt.rgba(1, 1, 1, 0.3) - border.width: inputField.activeFocus ? 2 : 1 - - GreeterUserPicker { - id: userPicker - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: root.userListOpen ? undefined : parent.verticalCenter - anchors.top: root.userListOpen ? parent.top : undefined - anchors.margins: Theme.spacingM - maxExpandedHeight: root.userPickerMaxHeight - visible: root.showUserPicker && !GreeterState.showPasswordInput - expanded: root.userListOpen - autoLoginVisible: root.autoLoginAvailable - autoLoginChecked: root.autoLoginOnSuccess - manualEntryVisible: true - onUserSelected: username => root.selectUser(username, false) - onToggleRequested: root.userListOpen = !root.userListOpen - onAutoLoginToggled: root.autoLoginOnSuccess = !root.autoLoginOnSuccess - onManualEntryRequested: root.enterManualUsernameEntry() - } - - DankIcon { - id: lockIcon - - anchors.left: parent.left - anchors.leftMargin: Theme.spacingM - anchors.verticalCenter: parent.verticalCenter - name: GreeterState.showPasswordInput ? "lock" : "person" - size: 20 - color: inputField.activeFocus ? Theme.primary : Theme.surfaceVariantText - visible: !root.showUserPicker - } - - TextInput { - id: inputField - - property bool syncingFromState: false - - anchors.fill: parent - anchors.leftMargin: lockIcon.width + Theme.spacingM * 2 - anchors.rightMargin: { - let margin = Theme.spacingM; - if (GreeterState.showPasswordInput && revealButton.visible) { - margin += revealButton.width; - } - if (externalAuthButton.visible) { - margin += externalAuthButton.width; - } - if (virtualKeyboardButton.visible) { - margin += virtualKeyboardButton.width; - } - if (enterButton.visible) { - margin += enterButton.width + 2; - } - return margin; - } - enabled: !root.showUserPicker || GreeterState.showPasswordInput - opacity: 0 - focus: !root.showUserPicker || GreeterState.showPasswordInput - echoMode: GreeterState.showPasswordInput ? (parent.showPassword ? TextInput.Normal : TextInput.Password) : TextInput.Normal - onTextChanged: { - if (syncingFromState) - return; - if (GreeterState.showPasswordInput) { - GreeterState.passwordBuffer = text; - if (!text || text.length === 0) - root.passwordSubmitRequested = false; - } else { - GreeterState.usernameInput = text; - } - } - onAccepted: { - if (GreeterState.showPasswordInput) { - root.startAuthSession(true); - } else { - if (text.trim()) { - root.submitUsername(text); - syncingFromState = true; - text = ""; - syncingFromState = false; - } - } - } - - Component.onCompleted: { - syncingFromState = true; - text = GreeterState.showPasswordInput ? GreeterState.passwordBuffer : GreeterState.usernameInput; - syncingFromState = false; - if (isPrimaryScreen && !powerMenu.isVisible) - forceActiveFocus(); - } - onVisibleChanged: { - if (visible && isPrimaryScreen && !powerMenu.isVisible) - forceActiveFocus(); - } - } - - KeyboardController { - id: keyboard_controller - target: inputField - rootObject: root - } - - StyledText { - id: placeholder - - anchors.left: lockIcon.right - anchors.leftMargin: Theme.spacingM - anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)))) - anchors.rightMargin: 2 - anchors.verticalCenter: parent.verticalCenter - text: { - if (GreeterState.unlocking) { - return I18n.tr("Logging in..."); - } - if (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse) { - return I18n.tr("Authenticating..."); - } - if (GreeterState.showPasswordInput) { - return I18n.tr("Password..."); - } - if (root.showUserPicker) { - return ""; - } - return I18n.tr("Username..."); - } - color: (GreeterState.unlocking || (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse)) ? Theme.primary : Theme.outline - font.pixelSize: Theme.fontSizeMedium - opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : (root.showUserPicker ? false : GreeterState.usernameInput.length === 0)) ? 1 : 0 - - Behavior on opacity { - NumberAnimation { - duration: Theme.mediumDuration - easing.type: Theme.standardEasing - } - } - - Behavior on color { - ColorAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing - } - } - } - - StyledText { - anchors.left: lockIcon.right - anchors.leftMargin: Theme.spacingM - anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)))) - anchors.rightMargin: 2 - anchors.verticalCenter: parent.verticalCenter - text: { - if (GreeterState.showPasswordInput) { - if (parent.showPassword) { - return GreeterState.passwordBuffer; - } - return "β€’".repeat(GreeterState.passwordBuffer.length); - } - return GreeterState.usernameInput; - } - color: Theme.surfaceText - font.pixelSize: (GreeterState.showPasswordInput && !parent.showPassword) ? Theme.fontSizeLarge : Theme.fontSizeMedium - opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length > 0 : (root.showUserPicker ? false : GreeterState.usernameInput.length > 0)) ? 1 : 0 - clip: true - elide: Text.ElideNone - horizontalAlignment: implicitWidth > width ? Text.AlignRight : Text.AlignLeft - - Behavior on opacity { - NumberAnimation { - duration: Theme.mediumDuration - easing.type: Theme.standardEasing - } - } - } - - DankActionButton { - id: revealButton - - anchors.right: externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)) - anchors.rightMargin: 0 - anchors.verticalCenter: parent.verticalCenter - iconName: parent.showPassword ? "visibility_off" : "visibility" - buttonSize: 32 - visible: GreeterState.showPasswordInput && GreeterState.passwordBuffer.length > 0 && (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking - enabled: visible - onClicked: parent.showPassword = !parent.showPassword - } - DankActionButton { - id: externalAuthButton - - anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right) - anchors.rightMargin: 0 - anchors.verticalCenter: parent.verticalCenter - iconName: root.greeterPamHasFprint ? "fingerprint" : "key" - buttonSize: 32 - visible: GreeterState.showPasswordInput && root.greeterExternalAuthAvailable && GreeterState.passwordBuffer.length === 0 && (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking - enabled: visible - onClicked: root.startAuthSession(false) - } - DankActionButton { - id: virtualKeyboardButton - - anchors.right: enterButton.visible ? enterButton.left : parent.right - anchors.rightMargin: enterButton.visible ? 0 : Theme.spacingS - anchors.verticalCenter: parent.verticalCenter - iconName: "keyboard" - buttonSize: 32 - visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking && (!root.showUserPicker || GreeterState.showPasswordInput) - enabled: visible - onClicked: { - if (keyboard_controller.isKeyboardActive) { - keyboard_controller.hide(); - } else { - keyboard_controller.show(); - } - } - } - - DankActionButton { - id: enterButton - - anchors.right: parent.right - anchors.rightMargin: 2 - anchors.verticalCenter: parent.verticalCenter - iconName: "keyboard_return" - buttonSize: 36 - visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking && (!root.showUserPicker || GreeterState.showPasswordInput) - enabled: true - onClicked: { - if (GreeterState.showPasswordInput) { - root.startAuthSession(true); - } else { - if (inputField.text.trim()) { - root.submitUsername(inputField.text); - inputField.text = ""; - } - } - } - - Behavior on opacity { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing - } - } - } - - Behavior on border.color { - ColorAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing - } - } - - Behavior on Layout.preferredHeight { - NumberAnimation { - duration: Theme.mediumDuration - easing.type: Theme.standardEasing - } - } - } - } - - Item { - Layout.fillWidth: true - Layout.preferredHeight: root.showAccountSwitchLink ? 28 : 0 - visible: root.showAccountSwitchLink - - StyledText { - id: accountSwitchLabel - - anchors.horizontalCenter: parent.horizontalCenter - text: I18n.tr("Back to user list", "greeter link to return from manual username entry to user picker") - color: Theme.primary - font.pixelSize: Theme.fontSizeSmall - font.underline: accountSwitchMouse.containsMouse - } - - MouseArea { - id: accountSwitchMouse - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: root.returnToUserListFromManualEntry() - } - } - - StyledText { - Layout.fillWidth: true - Layout.preferredHeight: root.authDisplayMessage !== "" ? 38 : 0 - Layout.topMargin: -Theme.spacingS - Layout.bottomMargin: -Theme.spacingS - text: root.authDisplayMessage - color: root.authFeedbackMessage !== "" ? Theme.error : (root.authSuccessMessage !== "" ? Theme.success : Theme.surfaceVariantText) - font.pixelSize: Theme.fontSizeSmall - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - maximumLineCount: 2 - opacity: root.authDisplayMessage !== "" ? 1 : 0 - - Behavior on opacity { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing - } - } - } - } - } - - Row { - anchors.top: parent.top - anchors.right: parent.right - anchors.margins: Theme.spacingXL - spacing: Theme.spacingL - - Item { - width: keyboardLayoutRow.width - height: keyboardLayoutRow.height - anchors.verticalCenter: parent.verticalCenter - visible: { - if (CompositorService.isNiri) { - return NiriService.keyboardLayoutNames.length > 1; - } else if (CompositorService.isHyprland) { - return hyprlandLayoutCount > 1; - } - return false; - } - - Row { - id: keyboardLayoutRow - spacing: Theme.spacingXS - - Item { - width: Theme.iconSize - height: Theme.iconSize - - DankIcon { - name: "keyboard" - size: Theme.iconSize - color: "white" - anchors.centerIn: parent - } - } - - Item { - width: childrenRect.width - height: Theme.iconSize - - StyledText { - text: { - if (CompositorService.isNiri) { - return LayoutCodes.layoutCode(NiriService.getCurrentKeyboardLayoutName()); - } else if (CompositorService.isHyprland) { - return hyprlandCurrentLayout; - } - return ""; - } - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Light - color: "white" - anchors.verticalCenter: parent.verticalCenter - } - } - } - - MouseArea { - id: keyboardLayoutArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (CompositorService.isNiri) { - NiriService.cycleKeyboardLayout(); - } else if (CompositorService.isHyprland) { - Quickshell.execDetached(["hyprctl", "switchxkblayout", hyprlandKeyboard, "next"]); - updateHyprlandLayout(); - } - } - } - } - - Rectangle { - width: 1 - height: 24 - color: Qt.rgba(255, 255, 255, 0.2) - anchors.verticalCenter: parent.verticalCenter - visible: { - const keyboardVisible = (CompositorService.isNiri && NiriService.keyboardLayoutNames.length > 1) || (CompositorService.isHyprland && hyprlandLayoutCount > 1); - return keyboardVisible && GreetdSettings.weatherEnabled && WeatherService.weather.available; - } - } - - Row { - spacing: Theme.spacingXS - visible: GreetdSettings.weatherEnabled && WeatherService.weather.available - anchors.verticalCenter: parent.verticalCenter - - DankIcon { - name: WeatherService.getWeatherIcon(WeatherService.weather.wCode) - size: Theme.iconSize - color: "white" - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: (GreetdSettings.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "Β°" - font.pixelSize: Theme.fontSizeLarge - font.weight: Font.Light - color: "white" - anchors.verticalCenter: parent.verticalCenter - } - } - - Rectangle { - width: 1 - height: 24 - color: Qt.rgba(255, 255, 255, 0.2) - anchors.verticalCenter: parent.verticalCenter - visible: GreetdSettings.weatherEnabled && WeatherService.weather.available && (NetworkService.networkStatus !== "disconnected" || BluetoothService.enabled || (AudioService.sink && AudioService.sink.audio) || BatteryService.batteryAvailable) - } - - Row { - spacing: Theme.spacingM - anchors.verticalCenter: parent.verticalCenter - visible: NetworkService.networkStatus !== "disconnected" || (BluetoothService.available && BluetoothService.enabled) || (AudioService.sink && AudioService.sink.audio) - - DankIcon { - name: NetworkService.networkStatus === "ethernet" ? "lan" : NetworkService.wifiSignalIcon - size: Theme.iconSize - 2 - color: NetworkService.networkStatus !== "disconnected" ? "white" : Qt.rgba(255, 255, 255, 0.5) - anchors.verticalCenter: parent.verticalCenter - visible: NetworkService.networkStatus !== "disconnected" - } - - DankIcon { - name: "bluetooth" - size: Theme.iconSize - 2 - color: "white" - anchors.verticalCenter: parent.verticalCenter - visible: BluetoothService.available && BluetoothService.enabled - } - - DankIcon { - name: { - if (!AudioService.sink?.audio) { - return "volume_up"; - } - if (AudioService.sink.audio.muted) - return "volume_off"; - if (AudioService.sink.audio.volume === 0) - return "volume_mute"; - if (AudioService.sink.audio.volume * 100 < 33) { - return "volume_down"; - } - return "volume_up"; - } - size: Theme.iconSize - 2 - color: (AudioService.sink && AudioService.sink.audio && (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0)) ? Qt.rgba(255, 255, 255, 0.5) : "white" - anchors.verticalCenter: parent.verticalCenter - visible: AudioService.sink && AudioService.sink.audio - } - } - - Rectangle { - width: 1 - height: 24 - color: Qt.rgba(255, 255, 255, 0.2) - anchors.verticalCenter: parent.verticalCenter - visible: BatteryService.batteryAvailable && (NetworkService.networkStatus !== "disconnected" || BluetoothService.enabled || (AudioService.sink && AudioService.sink.audio)) - } - - Row { - spacing: Theme.spacingXS - visible: BatteryService.batteryAvailable - anchors.verticalCenter: parent.verticalCenter - - DankIcon { - name: { - if (BatteryService.isCharging) { - if (BatteryService.batteryLevel >= 90) { - return "battery_charging_full"; - } - - if (BatteryService.batteryLevel >= 80) { - return "battery_charging_90"; - } - - if (BatteryService.batteryLevel >= 60) { - return "battery_charging_80"; - } - - if (BatteryService.batteryLevel >= 50) { - return "battery_charging_60"; - } - - if (BatteryService.batteryLevel >= 30) { - return "battery_charging_50"; - } - - if (BatteryService.batteryLevel >= 20) { - return "battery_charging_30"; - } - - return "battery_charging_20"; - } - if (BatteryService.isPluggedIn) { - if (BatteryService.batteryLevel >= 90) { - return "battery_charging_full"; - } - - if (BatteryService.batteryLevel >= 80) { - return "battery_charging_90"; - } - - if (BatteryService.batteryLevel >= 60) { - return "battery_charging_80"; - } - - if (BatteryService.batteryLevel >= 50) { - return "battery_charging_60"; - } - - if (BatteryService.batteryLevel >= 30) { - return "battery_charging_50"; - } - - if (BatteryService.batteryLevel >= 20) { - return "battery_charging_30"; - } - - return "battery_charging_20"; - } - if (BatteryService.batteryLevel >= 95) { - return "battery_full"; - } - - if (BatteryService.batteryLevel >= 85) { - return "battery_6_bar"; - } - - if (BatteryService.batteryLevel >= 70) { - return "battery_5_bar"; - } - - if (BatteryService.batteryLevel >= 55) { - return "battery_4_bar"; - } - - if (BatteryService.batteryLevel >= 40) { - return "battery_3_bar"; - } - - if (BatteryService.batteryLevel >= 25) { - return "battery_2_bar"; - } - - return "battery_1_bar"; - } - size: Theme.iconSize - color: { - if (BatteryService.isLowBattery && !BatteryService.isCharging) { - return Theme.error; - } - - if (BatteryService.isCharging || BatteryService.isPluggedIn) { - return Theme.primary; - } - - return "white"; - } - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: BatteryService.batteryLevel + "%" - font.pixelSize: Theme.fontSizeLarge - font.weight: Font.Light - color: "white" - anchors.verticalCenter: parent.verticalCenter - } - } - } - - DankActionButton { - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.margins: Theme.spacingXL - visible: GreetdSettings.lockScreenShowPowerActions - iconName: "power_settings_new" - iconColor: Theme.error - buttonSize: 40 - onClicked: powerMenu.show() - } - - Item { - anchors.bottom: parent.bottom - anchors.right: parent.right - anchors.margins: Theme.spacingXL - width: Math.max(200, currentSessionMetrics.width + 80) - height: 60 - - StyledTextMetrics { - id: currentSessionMetrics - text: root.currentSessionName - } - - property real longestSessionWidth: { - let maxWidth = 0; - for (var i = 0; i < sessionMetricsRepeater.count; i++) { - const item = sessionMetricsRepeater.itemAt(i); - if (item && item.width > maxWidth) { - maxWidth = item.width; - } - } - return maxWidth; - } - - Repeater { - id: sessionMetricsRepeater - model: GreeterState.sessionList - delegate: StyledTextMetrics { - text: modelData - } - } - - DankDropdown { - id: sessionDropdown - anchors.fill: parent - text: "" - description: "" - currentValue: root.currentSessionName - options: GreeterState.sessionList - enableFuzzySearch: GreeterState.sessionList.length > 5 - popupWidthOffset: 0 - popupWidth: Math.max(250, parent.longestSessionWidth + 100) - openUpwards: true - alignPopupRight: true - onValueChanged: value => { - const idx = GreeterState.sessionList.indexOf(value); - if (idx < 0) - return; - GreeterState.currentSessionIndex = idx; - GreeterState.selectedSession = GreeterState.sessionExecs[idx]; - GreeterState.selectedSessionPath = GreeterState.sessionPaths[idx]; - GreeterState.selectedSessionDesktopId = GreeterState.sessionDesktopIds[idx]; - } - } - } - } - - property string currentSessionName: GreeterState.sessionList[GreeterState.currentSessionIndex] || "" - - function finalizeSessionSelection() { - if (GreeterState.sessionList.length === 0) - return; - if (!GreetdMemory.memoryReady) - return; - if (!GreetdSettings.settingsLoaded) - return; - - const savedSession = GreetdSettings.rememberLastSession ? GreetdMemory.lastSessionId : ""; - const savedDesktopId = GreetdSettings.rememberLastSession ? (GreetdMemory.lastSessionDesktopId || desktopIdFromPath(GreetdMemory.lastSessionId)) : ""; - if ((savedSession || savedDesktopId) && GreetdSettings.rememberLastSession) { - for (var i = 0; i < GreeterState.sessionPaths.length; i++) { - if ((savedDesktopId && GreeterState.sessionDesktopIds[i] === savedDesktopId) || (savedSession && GreeterState.sessionPaths[i] === savedSession)) { - GreeterState.currentSessionIndex = i; - GreeterState.selectedSession = GreeterState.sessionExecs[i] || ""; - GreeterState.selectedSessionPath = GreeterState.sessionPaths[i]; - GreeterState.selectedSessionDesktopId = GreeterState.sessionDesktopIds[i] || ""; - return; - } - } - } - - GreeterState.currentSessionIndex = 0; - GreeterState.selectedSession = GreeterState.sessionExecs[0] || ""; - GreeterState.selectedSessionPath = GreeterState.sessionPaths[0] || ""; - GreeterState.selectedSessionDesktopId = GreeterState.sessionDesktopIds[0] || ""; - } - - property var sessionDirs: { - const homeDir = Quickshell.env("HOME") || ""; - const dirs = ["/usr/share/wayland-sessions", "/usr/share/xsessions", "/usr/local/share/wayland-sessions", "/usr/local/share/xsessions"]; - - if (homeDir) { - dirs.push(homeDir + "/.local/share/wayland-sessions"); - dirs.push(homeDir + "/.local/share/xsessions"); - } - - if (xdgDataDirs) { - xdgDataDirs.split(":").forEach(dir => { - if (dir) { - dirs.push(dir + "/wayland-sessions"); - dirs.push(dir + "/xsessions"); - } - }); - } - - // _addSession guards against a session name already existing - // so we have to load from the user directories first so they - // correctly override a system configuration - return dirs.reverse(); - } - - property var _pendingFiles: ({}) - property int _pendingCount: 0 - - function _addSession(path, name, exec) { - if (!name || !exec || GreeterState.sessionList.includes(name)) - return; - GreeterState.sessionList = GreeterState.sessionList.concat([name]); - GreeterState.sessionExecs = GreeterState.sessionExecs.concat([exec]); - GreeterState.sessionPaths = GreeterState.sessionPaths.concat([path]); - GreeterState.sessionDesktopIds = GreeterState.sessionDesktopIds.concat([desktopIdFromPath(path)]); - } - - function _parseDesktopFile(content, path) { - let name = ""; - let exec = ""; - const lines = content.split("\n"); - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (!name && line.startsWith("Name=")) - name = line.substring(5).trim(); - else if (!exec && line.startsWith("Exec=")) - exec = line.substring(5).trim(); - if (name && exec) - break; - } - _addSession(path, name, exec); - } - - function _loadDesktopFile(filePath) { - if (_pendingFiles[filePath]) - return; - _pendingFiles[filePath] = true; - _pendingCount++; - - const loader = desktopFileLoader.createObject(root, { - "filePath": filePath - }); - } - - function _onFileLoaded(filePath) { - _pendingCount--; - if (_pendingCount === 0) - Qt.callLater(finalizeSessionSelection); - } - - Component { - id: desktopFileLoader - - FileView { - id: fv - property string filePath: "" - path: filePath - - onLoaded: { - root._parseDesktopFile(text(), filePath); - root._onFileLoaded(filePath); - fv.destroy(); - } - - onLoadFailed: { - root._onFileLoaded(filePath); - fv.destroy(); - } - } - } - - Repeater { - model: isPrimaryScreen ? sessionDirs : [] - - Item { - required property string modelData - - FolderListModel { - folder: encodeFileUrl(modelData) - nameFilters: ["*.desktop"] - showDirs: false - showDotAndDotDot: false - - onStatusChanged: { - if (status !== FolderListModel.Ready) - return; - for (let i = 0; i < count; i++) { - let fp = get(i, "filePath"); - if (fp.startsWith("file://")) - fp = fp.substring(7); - root._loadDesktopFile(fp); - } - } - } - } - } - - Connections { - target: Greetd - enabled: isPrimaryScreen - - function onAuthMessage(message, error, responseRequired, echoResponse) { - if (responseRequired) { - cancelingExternalAuthForPassword = false; - passwordSessionTransitionRetryCount = 0; - awaitingExternalAuth = false; - pendingPasswordResponse = true; - const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0; - if (!passwordSubmitRequested && hasPasswordBuffer) - passwordSubmitRequested = true; - if (passwordSubmitRequested && !root.submitBufferedPassword()) - passwordSubmitRequested = false; - if (passwordSubmitRequested || hasPasswordBuffer) { - authTimeout.interval = defaultAuthTimeoutMs; - authTimeout.restart(); - } else { - authTimeout.stop(); - } - return; - } - pendingPasswordResponse = false; - const externalPrompt = root.isExternalAuthPrompt(message, responseRequired); - if (!passwordSubmitRequested) - awaitingExternalAuth = root.greeterExternalAuthAvailable && externalPrompt; - if (awaitingExternalAuth || (passwordSubmitRequested && externalPrompt && root.greeterPamHasExternalAuth)) - authTimeout.interval = externalAuthTimeoutMs; - else - authTimeout.interval = defaultAuthTimeoutMs; - authTimeout.restart(); - Greetd.respond(""); - } - - function onStateChanged() { - if (Greetd.state === GreetdState.Inactive) { - const resumePasswordSubmit = cancelingExternalAuthForPassword && passwordSubmitRequested; - awaitingExternalAuth = false; - pendingPasswordResponse = false; - cancelingExternalAuthForPassword = false; - authTimeout.interval = defaultAuthTimeoutMs; - authTimeout.stop(); - if (resumePasswordSubmit) { - Qt.callLater(function () { - root.startAuthSession(true); - }); - return; - } - resetPasswordSessionTransition(true); - } - } - - function onReadyToLaunch() { - awaitingExternalAuth = false; - pendingPasswordResponse = false; - resetPasswordSessionTransition(true); - authTimeout.interval = defaultAuthTimeoutMs; - authTimeout.stop(); - passwordFailureCount = 0; - clearAuthFeedback(); - authSuccessMessage = I18n.tr("Authenticated!"); - const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex]; - const sessionPath = GreeterState.selectedSessionPath || GreeterState.sessionPaths[GreeterState.currentSessionIndex]; - const sessionDesktopId = GreeterState.selectedSessionDesktopId || GreeterState.sessionDesktopIds[GreeterState.currentSessionIndex] || desktopIdFromPath(sessionPath); - if (!sessionCmd) { - GreeterState.pamState = "error"; - authFeedbackMessage = currentAuthMessage(); - placeholderDelay.restart(); - return; - } - - GreeterState.unlocking = true; - launchTimeout.restart(); - if (GreetdSettings.rememberLastSession) { - GreetdMemory.setLastSession(sessionPath, sessionDesktopId); - } else if (GreetdMemory.lastSessionId || GreetdMemory.lastSessionDesktopId || GreetdMemory.lastSessionExec) { - GreetdMemory.setLastSession("", ""); - } - if (GreetdSettings.rememberLastUser) { - GreetdMemory.setLastSuccessfulUser(GreeterState.username); - } else if (GreetdMemory.lastSuccessfulUser) { - GreetdMemory.setLastSuccessfulUser(""); - } - if (root.autoLoginOnSuccess) - greeterAutoLoginPendingProcess.running = true; - pendingLaunchCommand = sessionCmd; - pendingLaunchEnv = ["XDG_SESSION_TYPE=wayland"]; - if (Quickshell.env("DMS_VOID") === "1") - pendingLaunchEnv.push("LIBSEAT_BACKEND=logind"); - memoryFlushTimer.restart(); - } - - function onAuthFailure(message) { - awaitingExternalAuth = false; - pendingPasswordResponse = false; - resetPasswordSessionTransition(true); - authTimeout.interval = defaultAuthTimeoutMs; - authTimeout.stop(); - launchTimeout.stop(); - GreeterState.unlocking = false; - if (isLikelyLockoutMessage(message)) { - GreeterState.pamState = "max"; - } else { - GreeterState.pamState = "fail"; - passwordFailureCount = passwordFailureCount + 1; - } - authFeedbackMessage = currentAuthMessage(); - GreeterState.passwordBuffer = ""; - inputField.text = ""; - placeholderDelay.restart(); - Greetd.cancelSession(); - } - - function onError(error) { - awaitingExternalAuth = false; - pendingPasswordResponse = false; - resetPasswordSessionTransition(true); - authTimeout.interval = defaultAuthTimeoutMs; - authTimeout.stop(); - launchTimeout.stop(); - GreeterState.unlocking = false; - GreeterState.pamState = "error"; - authFeedbackMessage = currentAuthMessage(); - GreeterState.passwordBuffer = ""; - inputField.text = ""; - placeholderDelay.restart(); - Greetd.cancelSession(); - } - } - - Timer { - id: memoryFlushTimer - interval: memoryFlushDelayMs - onTriggered: { - if (!pendingLaunchCommand) - return; - const sessionCommand = pendingLaunchCommand; - const launchEnv = pendingLaunchEnv; - pendingLaunchCommand = ""; - pendingLaunchEnv = []; - const sessionArgs = sessionCommand.trim().split(/\s+/); - const needsVoidDbusSession = Quickshell.env("DMS_VOID") === "1" && !Quickshell.env("DBUS_SESSION_BUS_ADDRESS") && sessionArgs[0] !== "dbus-run-session"; - const launchArgs = needsVoidDbusSession ? ["dbus-run-session"].concat(sessionArgs) : sessionArgs; - Greetd.launch(launchArgs, launchEnv); - } - } - - Timer { - id: authTimeout - interval: defaultAuthTimeoutMs - onTriggered: { - if (GreeterState.unlocking || Greetd.state === GreetdState.Inactive) - return; - awaitingExternalAuth = false; - pendingPasswordResponse = false; - resetPasswordSessionTransition(true); - authTimeout.interval = defaultAuthTimeoutMs; - GreeterState.pamState = "error"; - authFeedbackMessage = currentAuthMessage(); - GreeterState.passwordBuffer = ""; - inputField.text = ""; - placeholderDelay.restart(); - Greetd.cancelSession(); - } - } - - Timer { - id: launchTimeout - interval: 8000 - onTriggered: { - if (!GreeterState.unlocking) - return; - pendingPasswordResponse = false; - resetPasswordSessionTransition(true); - GreeterState.unlocking = false; - GreeterState.pamState = "error"; - authFeedbackMessage = currentAuthMessage(); - placeholderDelay.restart(); - Greetd.cancelSession(); - } - } - - Timer { - id: placeholderDelay - interval: 4000 - onTriggered: clearAuthFeedback() - } - - LockPowerMenu { - id: powerMenu - showLogout: false - powerActionConfirmOverride: GreetdSettings.powerActionConfirm - powerActionHoldDurationOverride: GreetdSettings.powerActionHoldDuration - powerMenuActionsOverride: GreetdSettings.powerMenuActions - powerMenuDefaultActionOverride: GreetdSettings.powerMenuDefaultAction - powerMenuGridLayoutOverride: GreetdSettings.powerMenuGridLayout - requiredActions: ["poweroff"] - onClosed: { - if (isPrimaryScreen && inputField && inputField.forceActiveFocus) { - Qt.callLater(() => inputField.forceActiveFocus()); - } - } - } -} diff --git a/quickshell/Modules/Greetd/GreeterState.qml b/quickshell/Modules/Greetd/GreeterState.qml deleted file mode 100644 index 58cbe45ec..000000000 --- a/quickshell/Modules/Greetd/GreeterState.qml +++ /dev/null @@ -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; - } -} diff --git a/quickshell/Modules/Greetd/GreeterSurface.qml b/quickshell/Modules/Greetd/GreeterSurface.qml deleted file mode 100644 index a8af5df4c..000000000 --- a/quickshell/Modules/Greetd/GreeterSurface.qml +++ /dev/null @@ -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 ?? "" - } - } -} diff --git a/quickshell/Modules/Greetd/GreeterUserPicker.qml b/quickshell/Modules/Greetd/GreeterUserPicker.qml deleted file mode 100644 index b2b166681..000000000 --- a/quickshell/Modules/Greetd/GreeterUserPicker.qml +++ /dev/null @@ -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() - } - } - } -} diff --git a/quickshell/Modules/Greetd/GreeterUserTheme.qml b/quickshell/Modules/Greetd/GreeterUserTheme.qml deleted file mode 100644 index 9ee9ae011..000000000 --- a/quickshell/Modules/Greetd/GreeterUserTheme.qml +++ /dev/null @@ -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" : ""; - } -} diff --git a/quickshell/Modules/Greetd/README.md b/quickshell/Modules/Greetd/README.md deleted file mode 100644 index f23768849..000000000 --- a/quickshell/Modules/Greetd/README.md +++ /dev/null @@ -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 - -# 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 -``` - - - -### 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 : /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 `. Each added user must log out and back in before `--profile` will work. - -Per-user settings are stored under `/var/cache/dms-greeter/users//` 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 `:` with `2770` permissions. If the greeter user is not available yet, DMS falls back to `root:`. diff --git a/quickshell/Modules/Greetd/assets/dms-greeter b/quickshell/Modules/Greetd/assets/dms-greeter deleted file mode 100755 index 6a462a1f7..000000000 --- a/quickshell/Modules/Greetd/assets/dms-greeter +++ /dev/null @@ -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 diff --git a/quickshell/Modules/Greetd/assets/dms-hypr.conf b/quickshell/Modules/Greetd/assets/dms-hypr.conf deleted file mode 100644 index 03f6063ff..000000000 --- a/quickshell/Modules/Greetd/assets/dms-hypr.conf +++ /dev/null @@ -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" diff --git a/quickshell/Modules/Greetd/assets/dms-hypr.lua b/quickshell/Modules/Greetd/assets/dms-hypr.lua deleted file mode 100644 index f875d6370..000000000 --- a/quickshell/Modules/Greetd/assets/dms-hypr.lua +++ /dev/null @@ -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) diff --git a/quickshell/Modules/Greetd/assets/dms-niri.kdl b/quickshell/Modules/Greetd/assets/dms-niri.kdl deleted file mode 100644 index 2af568ec2..000000000 --- a/quickshell/Modules/Greetd/assets/dms-niri.kdl +++ /dev/null @@ -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" -} diff --git a/quickshell/Modules/Greetd/assets/greet-hyprland.sh b/quickshell/Modules/Greetd/assets/greet-hyprland.sh deleted file mode 100755 index 58fab7fb1..000000000 --- a/quickshell/Modules/Greetd/assets/greet-hyprland.sh +++ /dev/null @@ -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 diff --git a/quickshell/Modules/Greetd/assets/greet-niri.sh b/quickshell/Modules/Greetd/assets/greet-niri.sh deleted file mode 100755 index d31db65db..000000000 --- a/quickshell/Modules/Greetd/assets/greet-niri.sh +++ /dev/null @@ -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 diff --git a/quickshell/Modules/Lock/CustomButtonKeyboard.qml b/quickshell/Modules/Lock/CustomButtonKeyboard.qml deleted file mode 100644 index e6f1e80d3..000000000 --- a/quickshell/Modules/Lock/CustomButtonKeyboard.qml +++ /dev/null @@ -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 - } -} diff --git a/quickshell/Modules/Lock/Keyboard.qml b/quickshell/Modules/Lock/Keyboard.qml deleted file mode 100644 index b6f53e17b..000000000 --- a/quickshell/Modules/Lock/Keyboard.qml +++ /dev/null @@ -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; - } - } - } -} diff --git a/quickshell/Modules/Lock/KeyboardController.qml b/quickshell/Modules/Lock/KeyboardController.qml deleted file mode 100644 index d2d07c6a5..000000000 --- a/quickshell/Modules/Lock/KeyboardController.qml +++ /dev/null @@ -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 {} - } -} diff --git a/quickshell/Modules/Lock/LockPowerMenu.qml b/quickshell/Modules/Lock/LockPowerMenu.qml deleted file mode 100644 index a9898f217..000000000 --- a/quickshell/Modules/Lock/LockPowerMenu.qml +++ /dev/null @@ -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() -} diff --git a/quickshell/Modules/Lock/LockScreenContent.qml b/quickshell/Modules/Lock/LockScreenContent.qml index a9a7250d1..f64e94a8f 100644 --- a/quickshell/Modules/Lock/LockScreenContent.qml +++ b/quickshell/Modules/Lock/LockScreenContent.qml @@ -12,7 +12,8 @@ import qs.Common import qs.Modals import qs.Services import qs.Widgets -import "../../Common/LayoutCodes.js" as LayoutCodes +import qs.DankCommon.Session +import "../../DankCommon/Common/LayoutCodes.js" as LayoutCodes Item { id: root diff --git a/quickshell/Modules/Settings/GreeterTab.qml b/quickshell/Modules/Settings/GreeterTab.qml index 397f40834..97d6a23e7 100644 --- a/quickshell/Modules/Settings/GreeterTab.qml +++ b/quickshell/Modules/Settings/GreeterTab.qml @@ -82,27 +82,44 @@ Item { property bool greeterBinaryExists: false property bool greeterEnabled: false 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: { - if (!root.greeterInstalled) + switch (greeterAction) { + case "install": return I18n.tr("Install"); - if (!root.greeterEnabled) + case "activate": return I18n.tr("Activate"); - return I18n.tr("Uninstall"); + default: + return ""; + } } readonly property string greeterActionIcon: { - if (!root.greeterInstalled) + switch (greeterAction) { + case "install": return "download"; - if (!root.greeterEnabled) + case "activate": return "login"; - return "delete"; + default: + return ""; + } } readonly property var greeterActionCommand: { - if (!root.greeterInstalled) + switch (greeterAction) { + case "install": return ["dms", "greeter", "install", "--terminal"]; - if (!root.greeterEnabled) + case "activate": return ["dms", "greeter", "enable", "--terminal"]; - return ["dms", "greeter", "uninstall", "--terminal", "--yes"]; + default: + return []; + } } property string greeterPendingAction: "" @@ -120,26 +137,28 @@ Item { } function runGreeterInstallAction() { - root.greeterPendingAction = !root.greeterInstalled ? "install" : !root.greeterEnabled ? "activate" : "uninstall"; + root.greeterPendingAction = root.greeterAction; greeterStatusText = I18n.tr("Opening terminal: ") + root.greeterActionLabel + "..."; greeterInstallActionRunning = true; greeterInstallActionProcess.running = true; } function promptGreeterActionConfirm() { + if (!root.greeterActionAvailable) + return; + var title, message, confirmText; - if (!root.greeterInstalled) { + switch (root.greeterAction) { + case "install": title = I18n.tr("Install Greeter", "greeter action confirmation"); message = I18n.tr("Install the DMS greeter? A terminal will open for sudo authentication."); confirmText = I18n.tr("Install"); - } else if (!root.greeterEnabled) { + break; + case "activate": 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."); confirmText = I18n.tr("Activate"); - } else { - 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"); + break; } greeterActionConfirm.showWithOptions({ "title": title, @@ -247,16 +266,8 @@ Item { root.greeterSyncRunning = false; const out = (root.greeterSyncStdout || "").trim(); const err = (root.greeterSyncStderr || "").trim(); - if (exitCode === 0) { - var success = I18n.tr("Sync completed successfully."); - if (out !== "") - success = success + "\n\n" + out; - if (err !== "") - success = success + "\n\nstderr:\n" + err; - root.greeterStatusText = success; - SettingsData.clearGreeterSyncPending(); - ToastService.showInfo(I18n.tr("Greeter sync complete")); - } else { + 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; @@ -264,8 +275,16 @@ Item { failure = failure + "\n\nstderr:\n" + err; root.greeterStatusText = failure; root.launchGreeterSyncTerminalFallback(false, ""); + return; } - root.checkGreeterInstallState(); + var success = I18n.tr("Sync completed successfully."); + if (out !== "") + success = success + "\n\n" + out; + if (err !== "") + success = success + "\n\nstderr:\n" + err; + root.greeterStatusText = success; + SettingsData.clearGreeterSyncPending(); + ToastService.showInfo(I18n.tr("Greeter sync complete")); } } @@ -327,17 +346,19 @@ Item { root.greeterInstallActionRunning = false; const pending = 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(); + 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 anchors.fill: parent 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.family: "monospace" color: root.greeterStatusRunning ? Theme.surfaceVariantText : Theme.surfaceText @@ -442,6 +469,7 @@ Item { spacing: Theme.spacingS DankButton { + visible: root.greeterActionAvailable text: root.greeterActionLabel iconName: root.greeterActionIcon horizontalPadding: Theme.spacingL @@ -556,9 +584,13 @@ Item { description: I18n.tr("Format the date on the login screen") options: root._lockDateFormatPresets.map(p => p.label) 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); - 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 => { var preset = root._lockDateFormatPresets.find(p => p.label === value); diff --git a/quickshell/Modules/WallpaperBackground.qml b/quickshell/Modules/WallpaperBackground.qml index e755a2480..151369c7a 100644 --- a/quickshell/Modules/WallpaperBackground.qml +++ b/quickshell/Modules/WallpaperBackground.qml @@ -13,12 +13,7 @@ Variants { // An entry present in PanelWindow.onCompleted means we're recreating // after a wl_output rebind, not at initial startup. property var _seenScreens: ({}) - model: { - if (SessionData.isGreeterMode) { - return Quickshell.screens; - } - return SettingsData.getFilteredScreens("wallpaper"); - } + model: SettingsData.getFilteredScreens("wallpaper") PanelWindow { id: wallpaperWindow diff --git a/quickshell/README.md b/quickshell/README.md index df8529544..002d15ad6 100644 --- a/quickshell/README.md +++ b/quickshell/README.md @@ -42,7 +42,6 @@ make lint-qml # Run from repo root; requires quickshell/.qmlls.ini (generated b **System Controls** - `Modules/ControlCenter/` - WiFi, Bluetooth, audio, display settings - `Modules/Notifications/` - Notification center with popups -- `Modules/Greetd/` - Login greeter interface **Overlays** - `Modules/Spotlight/` - Application and file launcher diff --git a/quickshell/Services/GreeterUsersService.qml b/quickshell/Services/GreeterUsersService.qml deleted file mode 100644 index 9c6a6b8b9..000000000 --- a/quickshell/Services/GreeterUsersService.qml +++ /dev/null @@ -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); - } -} diff --git a/quickshell/Services/PortalService.qml b/quickshell/Services/PortalService.qml index ac9e5c958..82c7bc82c 100644 --- a/quickshell/Services/PortalService.qml +++ b/quickshell/Services/PortalService.qml @@ -53,10 +53,6 @@ Singleton { profileImage = ""; return; } - if (Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true") { - profileImage = ""; - return; - } if (!freedeskAvailable) { 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 { id: colorSchemeDetector command: ["bash", "-c", "command -v gsettings || command -v dconf"] diff --git a/quickshell/Services/WeatherService.qml b/quickshell/Services/WeatherService.qml index aabb669ea..f5955ffcd 100644 --- a/quickshell/Services/WeatherService.qml +++ b/quickshell/Services/WeatherService.qml @@ -5,7 +5,6 @@ import QtQuick import Quickshell import Quickshell.Io import qs.Common -import qs.Modules.Greetd import "../Common/suncalc.js" as SunCalc Singleton { @@ -463,7 +462,7 @@ Singleton { } function getConfiguredLocationName() { - return SessionData.isGreeterMode ? SessionData.weatherLocation : SettingsData.weatherLocation; + return SettingsData.weatherLocation; } function setLocation(lat, lon, city, country) { @@ -516,9 +515,9 @@ Singleton { } function updateLocation() { - const useAuto = SessionData.isGreeterMode ? GreetdSettings.useAutoLocation : SettingsData.useAutoLocation; - const coords = SessionData.isGreeterMode ? SessionData.weatherCoordinates : SettingsData.weatherCoordinates; - const cityName = SessionData.isGreeterMode ? SessionData.weatherLocation : SettingsData.weatherLocation; + const useAuto = SettingsData.useAutoLocation; + const coords = SettingsData.weatherCoordinates; + const cityName = SettingsData.weatherLocation; if (useAuto) { getLocationFromService(); @@ -1024,7 +1023,7 @@ Singleton { Timer { id: updateTimer interval: nextInterval() - running: root.refCount > 0 && SettingsData.weatherEnabled && !SessionData.isGreeterMode + running: root.refCount > 0 && SettingsData.weatherEnabled repeat: true triggeredOnStart: true onTriggered: { diff --git a/quickshell/scripts/i18nsync.py b/quickshell/scripts/i18nsync.py index e67c55e38..a58013eca 100755 --- a/quickshell/scripts/i18nsync.py +++ b/quickshell/scripts/i18nsync.py @@ -104,6 +104,26 @@ def load_common_entries(): entries = json.load(f) return [{**e, "tags": sorted(set(e.get("tags", [])) | {"dank-qml-common"})} for e in entries] +# dms-greeter terms live in this POEditor project but are owned by the +# dank-greeter repo. They must ride along in every upload so prune +# (sync_terms) does not delete them and tag-less uploads do not strip +# their tag. +GREETER_TAG = "dms-greeter" + +def load_greeter_entries(api_token, project_id): + resp = poeditor_request('terms/list', { + 'api_token': api_token, + 'id': project_id + }) + if resp.get('response', {}).get('status') != 'success': + error(f"POEditor terms list failed: {resp}") + terms = resp.get('result', {}).get('terms', []) + return [ + {'term': t['term'], 'context': t.get('context', ''), 'tags': sorted(set(t.get('tags', [])))} + for t in terms + if GREETER_TAG in t.get('tags', []) + ] + def entry_keys(entries): return {(e.get('context') or e['term'], e['term']) for e in entries} @@ -118,14 +138,15 @@ def combine_entries(app_entries, common_entries): combined.append(entry) return combined + list(common_by_key.values()) -def split_export(data, common_keys): +def split_export(data, common_keys, greeter_keys): app_part = {} common_part = {} for context, terms in data.items(): if not isinstance(terms, dict): continue common_terms = {t: v for t, v in terms.items() if (context, t) in common_keys} - app_terms = {t: v for t, v in terms.items() if (context, t) not in common_keys} + app_terms = {t: v for t, v in terms.items() + if (context, t) not in common_keys and (context, t) not in greeter_keys} if common_terms: common_part[context] = common_terms if app_terms: @@ -197,7 +218,7 @@ def write_if_changed(repo_file, new_data): f.write('\n') return True -def download_translations(api_token, project_id, common_keys): +def download_translations(api_token, project_id, common_keys, greeter_keys): info("Downloading translations from POEditor...") POEXPORTS_DIR.mkdir(parents=True, exist_ok=True) @@ -234,7 +255,7 @@ def download_translations(api_token, project_id, common_keys): warn(f"Failed to download {po_lang}: {e}") continue - app_part, common_part = split_export(new_data, common_keys) + app_part, common_part = split_export(new_data, common_keys, greeter_keys) if write_if_changed(repo_file, app_part): success(f"Updated {filename}") @@ -257,6 +278,7 @@ def check_sync_status(): current_en = normalize_json(EN_JSON) common_entries = load_common_entries() common_keys = entry_keys(common_entries) + greeter_keys = entry_keys(load_greeter_entries(api_token, project_id)) if not SYNC_STATE.exists(): return True @@ -294,7 +316,7 @@ def check_sync_status(): try: with request.urlopen(url) as response: remote_data = json.loads(response.read().decode()) - app_part, common_part = split_export(remote_data, common_keys) + app_part, common_part = split_export(remote_data, common_keys, greeter_keys) first_file = LANGUAGES[list(LANGUAGES.keys())[0]] if json_changed(POEXPORTS_DIR / first_file, app_part): @@ -366,9 +388,12 @@ def main(): warn("--prune deletes every POEditor term missing from the local en.json, including its translations.") warn("Terms from dms-plugins/ are machine-dependent: make sure all official plugins are present before pruning.") warn("dank-qml-common terms are included from the submodule, so pruning keeps them as long as the submodule is current.") + warn("dms-greeter terms are fetched from POEditor and re-included, so pruning keeps them.") common_entries = load_common_entries() common_keys = entry_keys(common_entries) + greeter_entries = load_greeter_entries(api_token, project_id) + greeter_keys = entry_keys(greeter_entries) extract_strings() @@ -396,11 +421,13 @@ def main(): common_changed = json.dumps(common_entries, sort_keys=True) != json.dumps(last_common_en, sort_keys=True) if strings_changed or common_changed or prune: - upload_source_strings(api_token, project_id, combine_entries(current_en, common_entries), prune) + combined = combine_entries(current_en, common_entries) + combined = combine_entries(combined, greeter_entries) + upload_source_strings(api_token, project_id, combined, prune) else: info("No changes in source strings") - translations_changed, common_files_changed = download_translations(api_token, project_id, common_keys) + translations_changed, common_files_changed = download_translations(api_token, project_id, common_keys, greeter_keys) if strings_changed or translations_changed: subprocess.run(['git', 'add', 'translations/'], cwd=REPO_ROOT) diff --git a/quickshell/scripts/qmllint-entrypoints.sh b/quickshell/scripts/qmllint-entrypoints.sh index 11b24c129..24c53616f 100755 --- a/quickshell/scripts/qmllint-entrypoints.sh +++ b/quickshell/scripts/qmllint-entrypoints.sh @@ -103,7 +103,6 @@ fi targets=( "${quickshell_dir}/shell.qml" "${quickshell_dir}/DMSShell.qml" - "${quickshell_dir}/DMSGreeter.qml" ) qmllint_args=( diff --git a/quickshell/shell.qml b/quickshell/shell.qml index 64add30a4..1ed0f0111 100644 --- a/quickshell/shell.qml +++ b/quickshell/shell.qml @@ -16,7 +16,6 @@ import qs.Services ShellRoot { id: entrypoint - readonly property bool runGreeter: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true" readonly property bool disableHotReload: Quickshell.env("DMS_DISABLE_HOT_RELOAD") === "1" || Quickshell.env("DMS_DISABLE_HOT_RELOAD") === "true" Component.onCompleted: { @@ -25,7 +24,6 @@ ShellRoot { Loader { id: wallpaperLoader - active: !entrypoint.runGreeter asynchronous: false sourceComponent: Scope { @@ -41,7 +39,6 @@ ShellRoot { Loader { id: shellCoreLoader - active: !entrypoint.runGreeter asynchronous: true source: "ShellCore.qml" onLoaded: dmsShellLoader.setSource("DMSShell.qml", { @@ -53,11 +50,4 @@ ShellRoot { id: dmsShellLoader asynchronous: true } - - Loader { - id: dmsGreeterLoader - active: entrypoint.runGreeter - asynchronous: false - source: "DMSGreeter.qml" - } } diff --git a/quickshell/systemd/sysusers-dms-greeter.conf b/quickshell/systemd/sysusers-dms-greeter.conf deleted file mode 100644 index 383d42ada..000000000 --- a/quickshell/systemd/sysusers-dms-greeter.conf +++ /dev/null @@ -1,3 +0,0 @@ -# DMS greeter system user for greetd (declarative; survives ostree /etc/passwd merge). -# Installed as /usr/lib/sysusers.d/dms-greeter.conf -u greeter - "System Greeter" /var/lib/greeter /bin/bash diff --git a/quickshell/systemd/tmpfiles-dms-greeter.conf b/quickshell/systemd/tmpfiles-dms-greeter.conf deleted file mode 100644 index f5f37541e..000000000 --- a/quickshell/systemd/tmpfiles-dms-greeter.conf +++ /dev/null @@ -1,3 +0,0 @@ -# Path Mode User Group Age Argument -d /var/cache/dms-greeter 0750 greeter greeter - -d /var/lib/greeter 0755 greeter greeter -