1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-01 19:18:28 -04:00

greeter: migrated dms-greeter to github/AvengeMedia/dank-greeter

This commit is contained in:
bbedward
2026-07-19 14:18:26 -04:00
parent 1402d2312a
commit a69acf171a
106 changed files with 441 additions and 14644 deletions
-2
View File
@@ -33,8 +33,6 @@ jobs:
- name: Run NixOS service start test - name: Run NixOS service start test
run: nix build .#nixosTests.x86_64-linux.nixos-service-start-module -L run: nix build .#nixosTests.x86_64-linux.nixos-service-start-module -L
- name: Run greeter niri test
run: nix build .#nixosTests.x86_64-linux.greeter-niri-module -L
- name: Run home-manager module test - name: Run home-manager module test
run: nix build .#nixosTests.x86_64-linux.home-manager-module -L run: nix build .#nixosTests.x86_64-linux.home-manager-module -L
+1 -9
View File
@@ -4,14 +4,12 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
package: package:
description: 'Package to build (dms, dms-greeter, or both)' description: 'Package to build'
required: false required: false
default: 'dms' default: 'dms'
type: choice type: choice
options: options:
- dms - dms
- dms-greeter
- both
version: version:
description: 'Versioning (e.g., 1.0.3, leave empty for latest release)' description: 'Versioning (e.g., 1.0.3, leave empty for latest release)'
required: false required: false
@@ -31,11 +29,7 @@ jobs:
id: set-packages id: set-packages
run: | run: |
PACKAGE_INPUT="${{ github.event.inputs.package || 'dms' }}" PACKAGE_INPUT="${{ github.event.inputs.package || 'dms' }}"
if [ "$PACKAGE_INPUT" = "both" ]; then
echo 'packages=["dms","dms-greeter"]' >> $GITHUB_OUTPUT
else
echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT
fi
build-and-upload: build-and-upload:
needs: determine-packages needs: determine-packages
@@ -163,8 +157,6 @@ jobs:
PACKAGE="${{ matrix.package }}" PACKAGE="${{ matrix.package }}"
if [ "$PACKAGE" = "dms" ]; then if [ "$PACKAGE" = "dms" ]; then
COPR_PROJECT="avengemedia/dms" COPR_PROJECT="avengemedia/dms"
elif [ "$PACKAGE" = "dms-greeter" ]; then
COPR_PROJECT="avengemedia/danklinux"
else else
echo "❌ Unknown package: $PACKAGE" echo "❌ Unknown package: $PACKAGE"
exit 1 exit 1
+9 -57
View File
@@ -9,7 +9,6 @@ on:
type: choice type: choice
options: options:
- dms - dms
- dms-greeter
- dms-git - dms-git
- all - all
default: "dms" default: "dms"
@@ -75,27 +74,12 @@ jobs:
fi fi
} }
# Helper function to check dms-greeter stable tag
check_dms_greeter_stable() {
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:danklinux/dms-greeter/dms-greeter.spec" 2>/dev/null || echo "")
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs | sed 's/^v//')
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "v$OBS_VERSION" ]]; then
echo "📋 dms-greeter: Tag $LATEST_TAG already exists, skipping"
return 1 # No update needed
else
echo "📋 dms-greeter: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
return 0 # Update needed
fi
}
# Main logic # Main logic
REBUILD="${{ github.event.inputs.rebuild_release }}" REBUILD="${{ github.event.inputs.rebuild_release }}"
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then
# Run from tag with no package specified - update both stable packages # Run from tag with no package specified - update stable package
echo "packages=dms dms-greeter" >> $GITHUB_OUTPUT echo "packages=dms" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}" VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT echo "has_updates=true" >> $GITHUB_OUTPUT
@@ -121,7 +105,7 @@ jobs:
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)" echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
elif [[ "$PKG" == "all" ]]; then elif [[ "$PKG" == "all" ]]; then
# Check each stable package and build list of those needing updates # Check the stable package and build list of those needing updates
PACKAGES_TO_UPDATE=() PACKAGES_TO_UPDATE=()
if check_dms_stable; then if check_dms_stable; then
PACKAGES_TO_UPDATE+=("dms") PACKAGES_TO_UPDATE+=("dms")
@@ -129,10 +113,6 @@ jobs:
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi fi
fi fi
if check_dms_greeter_stable; then
PACKAGES_TO_UPDATE+=("dms-greeter")
[[ -n "$LATEST_TAG" ]] && echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
@@ -141,7 +121,7 @@ jobs:
else else
echo "packages=" >> $GITHUB_OUTPUT echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT echo "has_updates=false" >> $GITHUB_OUTPUT
echo "✓ Both packages up to date" echo "✓ All packages up to date"
fi fi
elif [[ "$PKG" == "dms-git" ]]; then elif [[ "$PKG" == "dms-git" ]]; then
@@ -165,18 +145,6 @@ jobs:
echo "has_updates=false" >> $GITHUB_OUTPUT echo "has_updates=false" >> $GITHUB_OUTPUT
fi fi
elif [[ "$PKG" == "dms-greeter" ]]; then
if check_dms_greeter_stable; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
else else
# Unknown package - proceed anyway # Unknown package - proceed anyway
echo "packages=$PKG" >> $GITHUB_OUTPUT echo "packages=$PKG" >> $GITHUB_OUTPUT
@@ -221,9 +189,9 @@ jobs:
elif [[ -n "${{ github.event.inputs.package }}" ]]; then elif [[ -n "${{ github.event.inputs.package }}" ]]; then
# Manual workflow dispatch # Manual workflow dispatch
# Determine version for dms stable and dms-greeter using the API # Determine version for dms stable using the API
# GITHUB_REF is unreliable when "Use workflow from" a tag; API works from any ref # GITHUB_REF is unreliable when "Use workflow from" a tag; API works from any ref
if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "dms-greeter" ]] || [[ "${{ github.event.inputs.package }}" == "all" ]]; then if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "all" ]]; then
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "") LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
if [[ -n "$LATEST_TAG" ]]; then if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
@@ -289,7 +257,7 @@ jobs:
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE" echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
} > "distro/debian/dms-git/debian/changelog" } > "distro/debian/dms-git/debian/changelog"
- name: Update stable version (dms + dms-greeter) - name: Update stable version (dms)
if: steps.packages.outputs.version != '' if: steps.packages.outputs.version != ''
run: | run: |
VERSION="${{ steps.packages.outputs.version }}" VERSION="${{ steps.packages.outputs.version }}"
@@ -325,25 +293,12 @@ jobs:
fi fi
fi fi
# Update dms-greeter changelog when dms-greeter is in the upload list
if [[ "$PACKAGES" == *"dms-greeter"* ]] && [[ -f "distro/debian/dms-greeter/debian/changelog" ]]; then
CHANGELOG_DATE=$(date -R)
{
echo "dms-greeter (${VERSION_NO_V}db1) unstable; urgency=medium"
echo ""
echo " * Update to $VERSION stable release"
echo ""
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
} > "distro/debian/dms-greeter/debian/changelog"
echo "✓ Updated dms-greeter changelog to ${VERSION_NO_V}db1"
fi
# Update Debian _service files for packages in upload list (download_url paths) # Update Debian _service files for packages in upload list (download_url paths)
for service in distro/debian/*/_service; do for service in distro/debian/*/_service; do
if [[ -f "$service" ]]; then if [[ -f "$service" ]]; then
# Update tar_scm revision parameter (for dms-git) # Update tar_scm revision parameter (for dms-git)
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service" sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
# Update download_url paths (for dms, dms-greeter stable) # Update download_url paths (for dms stable)
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service" sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service" sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
fi fi
@@ -404,7 +359,7 @@ jobs:
UPLOADED_PACKAGES=() UPLOADED_PACKAGES=()
SKIPPED_PACKAGES=() SKIPPED_PACKAGES=()
# PACKAGES can be space-separated list (e.g., "dms dms-greeter" from "all" check) # PACKAGES can be a space-separated list (from the "all" check)
# Loop through each package and upload # Loop through each package and upload
for PKG in $PACKAGES; do for PKG in $PACKAGES; do
echo "" echo ""
@@ -507,9 +462,6 @@ jobs:
dms-git) dms-git)
echo "- $STATUS_ICON **dms-git** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY echo "- $STATUS_ICON **dms-git** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
;; ;;
dms-greeter)
echo "- $STATUS_ICON **dms-greeter** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:danklinux/dms-greeter)" >> $GITHUB_STEP_SUMMARY
;;
esac esac
done done
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
-3
View File
@@ -9,7 +9,6 @@ on:
type: choice type: choice
options: options:
- dms - dms
- dms-greeter
- dms-git - dms-git
- all - all
default: "dms" default: "dms"
@@ -135,7 +134,6 @@ jobs:
case "$PACKAGE" in case "$PACKAGE" in
dms) PPA_NAME="dms" ;; dms) PPA_NAME="dms" ;;
dms-git) PPA_NAME="dms-git" ;; dms-git) PPA_NAME="dms-git" ;;
dms-greeter) PPA_NAME="danklinux" ;;
*) echo "::error::Unknown package $PACKAGE"; exit 1 ;; *) echo "::error::Unknown package $PACKAGE"; exit 1 ;;
esac esac
@@ -150,4 +148,3 @@ jobs:
echo "- **Target:** ${{ matrix.target }}" >> "$GITHUB_STEP_SUMMARY" echo "- **Target:** ${{ matrix.target }}" >> "$GITHUB_STEP_SUMMARY"
echo "- **DMS PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> "$GITHUB_STEP_SUMMARY" echo "- **DMS PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> "$GITHUB_STEP_SUMMARY"
echo "- **DMS-Git PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> "$GITHUB_STEP_SUMMARY" echo "- **DMS-Git PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> "$GITHUB_STEP_SUMMARY"
echo "- **DankLinux PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> "$GITHUB_STEP_SUMMARY"
+4 -32
View File
@@ -27,11 +27,6 @@ on:
type: boolean type: boolean
required: false required: false
default: true default: true
build_greeter:
description: 'Build stable dms-greeter package'
type: boolean
required: false
default: true
permissions: permissions:
contents: read contents: read
@@ -106,7 +101,6 @@ jobs:
- name: Inject templates - name: Inject templates
run: | run: |
cp -R distro/void/srcpkgs/dms void-packages/srcpkgs/ cp -R distro/void/srcpkgs/dms void-packages/srcpkgs/
cp -R distro/void/srcpkgs/dms-greeter void-packages/srcpkgs/
cp -R distro/void/srcpkgs/dms-git void-packages/srcpkgs/ cp -R distro/void/srcpkgs/dms-git void-packages/srcpkgs/
- name: Enable unprivileged user namespaces (Ubuntu 24.04) - name: Enable unprivileged user namespaces (Ubuntu 24.04)
@@ -180,19 +174,16 @@ jobs:
mkdir -p r2-repo/current mkdir -p r2-repo/current
BUILD_DMS="${{ github.event.inputs.build_dms || 'true' }}" BUILD_DMS="${{ github.event.inputs.build_dms || 'true' }}"
BUILD_GREETER="${{ github.event.inputs.build_greeter || 'true' }}"
BUILD_GIT="${{ github.event.inputs.build_git || 'true' }}" BUILD_GIT="${{ github.event.inputs.build_git || 'true' }}"
FORCE_REBUILD="${{ github.event.inputs.force_rebuild || 'false' }}" FORCE_REBUILD="${{ github.event.inputs.force_rebuild || 'false' }}"
if [ "${{ github.event_name }}" = "schedule" ]; then if [ "${{ github.event_name }}" = "schedule" ]; then
BUILD_DMS="false" BUILD_DMS="false"
BUILD_GREETER="false"
BUILD_GIT="true" BUILD_GIT="true"
fi fi
if [ "${{ github.event_name }}" = "release" ]; then if [ "${{ github.event_name }}" = "release" ]; then
BUILD_DMS="true" BUILD_DMS="true"
BUILD_GREETER="true"
BUILD_GIT="false" BUILD_GIT="false"
fi fi
@@ -217,23 +208,20 @@ jobs:
echo "=== Starting Builds ===" echo "=== Starting Builds ==="
echo "DMS stable build enabled: $BUILD_DMS" echo "DMS stable build enabled: $BUILD_DMS"
echo "Greeter stable build enabled: $BUILD_GREETER"
echo "Git build enabled: $BUILD_GIT" echo "Git build enabled: $BUILD_GIT"
echo "Force rebuild: $FORCE_REBUILD" echo "Force rebuild: $FORCE_REBUILD"
cd void-packages cd void-packages
if [ -n "$RELEASE_VER" ] && { [ "$BUILD_DMS" = "true" ] || [ "$BUILD_GREETER" = "true" ]; }; then if [ -n "$RELEASE_VER" ] && [ "$BUILD_DMS" = "true" ]; then
echo "🔧 Updating stable templates for $ARCHIVE_TAG" echo "🔧 Updating stable template for $ARCHIVE_TAG"
TARBALL="$(mktemp)" TARBALL="$(mktemp)"
curl -fsSL -o "$TARBALL" "https://github.com/${{ github.repository }}/releases/download/${ARCHIVE_TAG}/dms-source.tar.gz" curl -fsSL -o "$TARBALL" "https://github.com/${{ github.repository }}/releases/download/${ARCHIVE_TAG}/dms-source.tar.gz"
RELEASE_CHECKSUM="$(sha256sum "$TARBALL" | cut -d' ' -f1)" RELEASE_CHECKSUM="$(sha256sum "$TARBALL" | cut -d' ' -f1)"
rm -f "$TARBALL" rm -f "$TARBALL"
for pkg in dms dms-greeter; do sed -i "s/^version=.*/version=${RELEASE_VER}/" srcpkgs/dms/template
sed -i "s/^version=.*/version=${RELEASE_VER}/" "srcpkgs/${pkg}/template" sed -i "s/^checksum=.*/checksum=${RELEASE_CHECKSUM}/" srcpkgs/dms/template
sed -i "s/^checksum=.*/checksum=${RELEASE_CHECKSUM}/" "srcpkgs/${pkg}/template"
done
fi fi
# 1. Build dms-git (development package) # 1. Build dms-git (development package)
@@ -289,22 +277,6 @@ jobs:
fi fi
fi fi
# 3. Build stable dms-greeter package
if [ "$BUILD_GREETER" = "true" ]; then
GREETER_VER=$(grep -E '^version=' srcpkgs/dms-greeter/template | cut -d= -f2 | tr -d '"')
GREETER_REV=$(grep -E '^revision=' srcpkgs/dms-greeter/template | cut -d= -f2 | tr -d '"')
EXPECTED_GREETER_FILE="dms-greeter-${GREETER_VER}_${GREETER_REV}.x86_64.xbps"
if [ -f "../r2-repo/current/$EXPECTED_GREETER_FILE" ] && [ "$FORCE_REBUILD" != "true" ]; then
echo "✅ $EXPECTED_GREETER_FILE already exists, skipping build."
else
echo "🔨 Compiling dms-greeter ($GREETER_VER)..."
./xbps-src pkg dms-greeter
rm -f "../r2-repo/current/${EXPECTED_GREETER_FILE}"
cp -L hostdir/binpkgs/dms-greeter-${GREETER_VER}_${GREETER_REV}.x86_64.xbps ../r2-repo/current/
fi
fi
- name: Index and sign repository - name: Index and sign repository
run: | run: |
cd r2-repo/current cd r2-repo/current
+12 -1
View File
@@ -98,7 +98,7 @@ Notification center with grouping, rich text support, and keyboard navigation.
MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews. MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews.
**Session Management** **Session Management**
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and greeter support. Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and a settings front-end for [dank-greeter](https://github.com/AvengeMedia/dank-greeter).
**Plugin System** **Plugin System**
Extend functionality with the [plugin registry](https://plugins.danklinux.com). Extend functionality with the [plugin registry](https://plugins.danklinux.com).
@@ -132,6 +132,17 @@ dms plugins search # Browse plugin registry
- **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview) - **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview)
- **Support:** [Ko-fi](https://ko-fi.com/avengemediallc) - **Support:** [Ko-fi](https://ko-fi.com/avengemediallc)
## Dank Projects
DMS is one piece of the suite. The rest lives in its own repos:
- [dank-greeter](https://github.com/AvengeMedia/dank-greeter) - greetd login screen with the Dank Material aesthetic. The Greeter tab in DMS settings is the front-end for it.
- [dankcalendar](https://github.com/AvengeMedia/dankcalendar) - Local, Google, Microsoft, and CalDAV calendars for the dank desktop.
- [dgop](https://github.com/AvengeMedia/dgop) - System monitoring CLI and API that powers the process list and dashboard widgets.
- [dsearch](https://github.com/AvengeMedia/danksearch) - Fast file search that powers file results in the launcher.
- [dank-qml-common](https://github.com/AvengeMedia/dank-qml-common) - Shared QML widgets and components used by DMS, dank-greeter, and dankcalendar.
- [dankgo](https://github.com/AvengeMedia/dankgo) - Common Go modules behind the single binary apps.
## Development ## Development
See component-specific documentation: See component-specific documentation:
+3 -3
View File
@@ -78,7 +78,7 @@ Custom IPC via unix socket (JSON API) for shell communication.
- `dms brightness [list|set]` - Control display/monitor brightness - `dms brightness [list|set]` - Control display/monitor brightness
- `dms color pick` - Native color picker (see below) - `dms color pick` - Native color picker (see below)
- `dms update` - Update DMS and dependencies (disabled in distro packages) - `dms update` - Update DMS and dependencies (disabled in distro packages)
- `dms greeter install` - Install greetd greeter (disabled in distro packages) - `dms greeter` - Deprecated; forwards to the standalone [dms-greeter](https://github.com/AvengeMedia/dank-greeter) binary
### Color Picker ### Color Picker
@@ -109,7 +109,7 @@ make test # Run tests
**Distribution build:** **Distribution build:**
```bash ```bash
make dist # Build without update/greeter features make dist # Build without update features
``` ```
Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64` Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64`
@@ -170,7 +170,7 @@ sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c hyprland -t ki
| `--exclude-deps <name,...>` | | Skip specific dependencies | | `--exclude-deps <name,...>` | | Skip specific dependencies |
| `--replace-configs <name,...>` | | Replace specific configuration files (mutually exclusive with `--replace-configs-all`) | | `--replace-configs <name,...>` | | Replace specific configuration files (mutually exclusive with `--replace-configs-all`) |
| `--replace-configs-all` | | Replace all configuration files (mutually exclusive with `--replace-configs`) | | `--replace-configs-all` | | Replace all configuration files (mutually exclusive with `--replace-configs`) |
| `--yes` | `-y` | Required for headless mode confirms installation without interactive prompts | | `--yes` | `-y` | Required for headless mode - confirms installation without interactive prompts |
Headless mode requires `--yes` to proceed; without it, the installer exits with an error. Headless mode requires `--yes` to proceed; without it, the installer exits with an error.
Configuration files are not replaced by default unless `--replace-configs` or `--replace-configs-all` is specified. Configuration files are not replaced by default unless `--replace-configs` or `--replace-configs-all` is specified.
+30 -1
View File
@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -15,7 +16,7 @@ import (
var authCmd = &cobra.Command{ var authCmd = &cobra.Command{
Use: "auth", Use: "auth",
Short: "Manage DMS authentication sync", Short: "Manage DMS authentication sync",
Long: "Manage shared PAM/authentication setup for DMS greeter and lock screen", Long: "Manage PAM/authentication setup for the DMS lock screen",
} }
var authSyncCmd = &cobra.Command{ var authSyncCmd = &cobra.Command{
@@ -211,3 +212,31 @@ func syncAuthInTerminal(nonInteractive bool) error {
shellCmd := shellSyncCmd + `; echo; echo "Authentication sync finished. Closing in 3 seconds..."; sleep 3` shellCmd := shellSyncCmd + `; echo; echo "Authentication sync finished. Closing in 3 seconds..."; sleep 3`
return runCommandInTerminal(shellCmd) return runCommandInTerminal(shellCmd)
} }
func runCommandInTerminal(shellCmd string) error {
terminals := []struct {
name string
args []string
}{
{"gnome-terminal", []string{"--", "bash", "-c", shellCmd}},
{"konsole", []string{"-e", "bash", "-c", shellCmd}},
{"xfce4-terminal", []string{"-e", "bash -c \"" + strings.ReplaceAll(shellCmd, `"`, `\"`) + "\""}},
{"ghostty", []string{"-e", "bash", "-c", shellCmd}},
{"wezterm", []string{"start", "--", "bash", "-c", shellCmd}},
{"alacritty", []string{"-e", "bash", "-c", shellCmd}},
{"kitty", []string{"bash", "-c", shellCmd}},
{"xterm", []string{"-e", "bash -c \"" + strings.ReplaceAll(shellCmd, `"`, `\"`) + "\""}},
}
for _, t := range terminals {
if _, err := exec.LookPath(t.name); err != nil {
continue
}
cmd := exec.Command(t.name, t.args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return err
}
return nil
}
return fmt.Errorf("no terminal emulator found (tried: gnome-terminal, konsole, xfce4-terminal, ghostty, wezterm, alacritty, kitty, xterm)")
}
File diff suppressed because it is too large Load Diff
-150
View File
@@ -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)
}
}
+19 -6
View File
@@ -11,7 +11,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc" "github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils" "github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
@@ -205,7 +204,12 @@ func detectTerminal() (string, error) {
} }
func detectCompositorForSetup() (string, error) { func detectCompositorForSetup() (string, error) {
compositors := greeter.DetectCompositors() var compositors []string
for _, candidate := range []string{"niri", "Hyprland", "mango"} {
if utils.CommandExists(candidate) {
compositors = append(compositors, candidate)
}
}
switch len(compositors) { switch len(compositors) {
case 0: case 0:
@@ -214,11 +218,20 @@ func detectCompositorForSetup() (string, error) {
return strings.ToLower(compositors[0]), nil return strings.ToLower(compositors[0]), nil
} }
selected, err := greeter.PromptCompositorChoice(compositors) fmt.Println("Multiple compositors detected:")
if err != nil { for i, compositor := range compositors {
return "", err fmt.Printf("%d) %s\n", i+1, compositor)
} }
return strings.ToLower(selected), nil fmt.Printf("\nChoice (1-%d): ", len(compositors))
var response string
fmt.Scanln(&response)
choice := 0
fmt.Sscanf(strings.TrimSpace(response), "%d", &choice)
if choice < 1 || choice > len(compositors) {
return "", fmt.Errorf("invalid choice")
}
return strings.ToLower(compositors[choice-1]), nil
} }
func runSetupDmsConfig(name string) error { func runSetupDmsConfig(name string) error {
-137
View File
@@ -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"
}
-1
View File
@@ -12,7 +12,6 @@ import (
var Version = "dev" var Version = "dev"
func init() { func init() {
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd)
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd) authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
updateCmd.AddCommand(updateCheckCmd) updateCmd.AddCommand(updateCheckCmd)
-1
View File
@@ -12,7 +12,6 @@ import (
var Version = "dev" var Version = "dev"
func init() { func init() {
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd)
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd) authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
@@ -1,17 +0,0 @@
hotkey-overlay {
skip-at-startup
}
environment {
DMS_RUN_GREETER "1"
}
gestures {
hot-corners {
off
}
}
layout {
background-color "#000000"
}
-3
View File
@@ -16,6 +16,3 @@ var NiriAlttabConfig string
//go:embed embedded/niri-binds.kdl //go:embed embedded/niri-binds.kdl
var NiriBindsConfig string var NiriBindsConfig string
//go:embed embedded/niri-greeter.kdl
var NiriGreeterConfig string
@@ -1,91 +0,0 @@
# AppArmor profile for dms-greeter
#
# Managed by DMS — regenerated on every `dms greeter install` / `dms greeter sync`.
# Manual edits will be overwritten on next sync.
#
# Mode: complain (denials are logged, nothing is blocked)
# To switch to enforce after validating with `aa-logprof`:
# sudo aa-enforce /etc/apparmor.d/usr.bin.dms-greeter
#
#include <tunables/global>
profile dms-greeter /usr/bin/dms-greeter flags=(complain) {
#include <abstractions/base>
#include <abstractions/bash>
# The launcher script itself
/usr/bin/dms-greeter r,
# Cache directory — created by dms greeter sync/enable with greeter:greeter ownership
/var/cache/dms-greeter/ rw,
/var/cache/dms-greeter/** rwlk,
# DMS config — packaged path
/usr/share/quickshell/dms-greeter/ r,
/usr/share/quickshell/dms-greeter/** r,
/usr/share/quickshell/ r,
/usr/share/quickshell/** r,
# DMS config — system and user overrides
/etc/dms/ r,
/etc/dms/** r,
/usr/share/dms/ r,
/usr/share/dms/** r,
/home/*/.config/quickshell/ r,
/home/*/.config/quickshell/** r,
/root/.config/quickshell/ r,
/root/.config/quickshell/** r,
# greetd / PAM — read-only for session setup
/etc/greetd/ r,
/etc/greetd/** r,
/etc/pam.d/ r,
/etc/pam.d/** r,
/usr/lib/pam.d/ r,
/usr/lib/pam.d/** r,
# Compositor binaries — run unconfined so each compositor uses its own profile
/usr/bin/niri Ux,
/usr/bin/hyprland Ux,
/usr/bin/Hyprland Ux,
/usr/bin/sway Ux,
/usr/bin/labwc Ux,
/usr/bin/scroll Ux,
/usr/bin/miracle-wm Ux,
/usr/bin/mango Ux,
# Quickshell — run unconfined (has its own compositor profile on some distros)
/usr/bin/qs Ux,
/usr/bin/quickshell Ux,
# Wayland / XDG runtime (pipewire, wireplumber, wayland socket)
/run/user/[0-9]*/ rw,
/run/user/[0-9]*/** rw,
# DRM / GPU devices (required for Wayland compositor startup)
/dev/dri/ r,
/dev/dri/* rw,
/dev/udmabuf rw,
# Input devices
/dev/input/ r,
/dev/input/* r,
# Systemd journal / logging
/run/systemd/journal/socket rw,
/dev/log rw,
# Shell helper binaries invoked by the launcher script
/usr/bin/env ix,
/usr/bin/mkdir ix,
/usr/bin/cat ix,
/usr/bin/grep ix,
/usr/bin/dirname ix,
/usr/bin/basename ix,
/usr/bin/command ix,
/bin/env ix,
/bin/mkdir ix,
# Signal management (compositor lifecycle)
signal (send, receive) set=("term", "int", "hup", "kill"),
}
File diff suppressed because it is too large Load Diff
-350
View File
@@ -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))
}
}
-200
View File
@@ -1,200 +0,0 @@
package greeter
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
)
func sessionDesktopIDFromPath(path string) string {
id := strings.TrimSpace(path)
if id == "" {
return ""
}
if strings.ContainsAny(id, "/\\") {
id = filepath.Base(id)
}
if id == "" {
return ""
}
if !strings.HasSuffix(id, ".desktop") {
id += ".desktop"
}
return id
}
func sessionDesktopIDFromMemory(mem greeterAutoLoginMemory) string {
if id := sessionDesktopIDFromPath(mem.LastSessionDesktopID); id != "" {
return id
}
return sessionDesktopIDFromPath(mem.LastSessionID)
}
func sessionDesktopDirs() []string {
seen := make(map[string]bool)
dirs := make([]string, 0, 8)
addBase := func(base string) {
base = strings.TrimSpace(base)
if base == "" {
return
}
for _, sub := range []string{"wayland-sessions", "xsessions"} {
dir := filepath.Join(base, sub)
if seen[dir] {
continue
}
seen[dir] = true
dirs = append(dirs, dir)
}
}
if dataHome := os.Getenv("XDG_DATA_HOME"); dataHome != "" {
addBase(dataHome)
} else if home, err := os.UserHomeDir(); err == nil && home != "" {
addBase(filepath.Join(home, ".local", "share"))
}
if dataDirs := os.Getenv("XDG_DATA_DIRS"); dataDirs != "" {
for _, dir := range strings.Split(dataDirs, ":") {
addBase(dir)
}
} else {
addBase("/usr/local/share")
addBase("/usr/share")
}
return dirs
}
func ResolveSessionExec(sessionID string) (string, error) {
return resolveSessionExecInDirs(sessionID, sessionDesktopDirs())
}
func resolveSessionExecInDirs(sessionID string, dirs []string) (string, error) {
id := sessionDesktopIDFromPath(sessionID)
if id == "" {
return "", fmt.Errorf("session id is empty")
}
for _, dir := range dirs {
path := filepath.Join(dir, id)
execLine, err := execFromDesktopFile(path)
if err == nil {
return execLine, nil
}
if !os.IsNotExist(err) {
return "", err
}
}
return "", fmt.Errorf("session desktop file %q was not found", id)
}
// parseExecString splits a Desktop Entry Exec= value into argv without
// involving a shell, mirroring quickshell's DesktopEntry::parseExecString
// (string quoting, value escapes, field code stripping).
func parseExecString(execLine string) []string {
var args []string
var cur strings.Builder
inString := false
escape := 0
percent := false
for _, c := range execLine {
switch {
case escape == 0 && c == '\\':
escape = 1
case inString:
switch {
case c == '\\':
escape++
if escape == 4 {
cur.WriteByte('\\')
escape = 0
}
case escape == 2:
cur.WriteRune(c)
escape = 0
case escape != 0:
switch c {
case 's':
cur.WriteByte(' ')
case 'n':
cur.WriteByte('\n')
case 't':
cur.WriteByte('\t')
case 'r':
cur.WriteByte('\r')
default:
cur.WriteRune(c)
}
escape = 0
case c == '"' || c == '\'':
inString = false
default:
cur.WriteRune(c)
}
case escape != 0:
cur.WriteRune(c)
escape = 0
case percent:
if c == '%' {
cur.WriteByte('%')
}
percent = false
case c == '%':
percent = true
case c == '"' || c == '\'':
inString = true
case c == ' ':
if cur.Len() > 0 {
args = append(args, cur.String())
cur.Reset()
}
default:
cur.WriteRune(c)
}
}
if cur.Len() > 0 {
args = append(args, cur.String())
}
return args
}
func LaunchSessionByID(sessionID string) error {
execLine, err := ResolveSessionExec(sessionID)
if err != nil {
return err
}
argv := parseExecString(strings.TrimSpace(execLine))
if len(argv) == 0 {
return fmt.Errorf("session %q has an empty Exec command", sessionID)
}
resolved, err := exec.LookPath(argv[0])
if err != nil {
return fmt.Errorf("session %q command %q not found: %w", sessionID, argv[0], err)
}
env := append(os.Environ(), "XDG_SESSION_TYPE=wayland")
return syscall.Exec(resolved, argv, env)
}
func LaunchSessionFromMemory(cacheDir, homeDir string) error {
enabled, _, sessionID, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
if err != nil {
return err
}
if !enabled {
return fmt.Errorf("greeter auto-login is disabled")
}
if sessionID == "" {
return fmt.Errorf("greeter auto-login has no remembered session")
}
return LaunchSessionByID(sessionID)
}
@@ -1,57 +0,0 @@
package greeter
import (
"path/filepath"
"reflect"
"testing"
)
func TestParseExecString(t *testing.T) {
t.Parallel()
tests := []struct {
name string
exec string
want []string
}{
{"plain", "niri --session", []string{"niri", "--session"}},
{"extra spaces", "niri --session", []string{"niri", "--session"}},
{"double quoted arg", `env "with space" run`, []string{"env", "with space", "run"}},
{"single quoted arg", `env 'with space' run`, []string{"env", "with space", "run"}},
{"escaped quote in quotes", `sh "say \\"hi\\""`, []string{"sh", `say "hi"`}},
{"field code dropped", "gnome-session %U", []string{"gnome-session"}},
{"field code mid-arg", "app --url=%u --run", []string{"app", "--url=", "--run"}},
{"literal percent", "app 100%% done", []string{"app", "100%", "done"}},
{"shell metachars stay literal", "sh -c $(reboot); echo", []string{"sh", "-c", "$(reboot);", "echo"}},
{"empty", "", nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := parseExecString(tt.exec); !reflect.DeepEqual(got, tt.want) {
t.Fatalf("parseExecString(%q) = %#v, want %#v", tt.exec, got, tt.want)
}
})
}
}
func TestExecFromDesktopFileOnlyReadsDesktopEntryGroup(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "example.desktop")
writeTestFile(t, path, `[Desktop Action other]
Exec=/wrong/binary
[Desktop Entry]
Name=Example
Exec = /right/binary --flag
`)
got, err := execFromDesktopFile(path)
if err != nil {
t.Fatalf("execFromDesktopFile returned error: %v", err)
}
if got != "/right/binary --flag" {
t.Fatalf("execFromDesktopFile = %q, want %q", got, "/right/binary --flag")
}
}
-548
View File
@@ -1,548 +0,0 @@
package greeter
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"regexp"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
var monitorWallpaperSanitizer = regexp.MustCompile(`[^a-zA-Z0-9]+`)
func userGreeterCacheDir(cacheDir, username string) string {
return filepath.Join(cacheDir, "users", username)
}
func isUserOwnedGreeterCacheSlot(path, username string) bool {
if strings.TrimSpace(username) == "" {
return false
}
userDir, err := filepath.Abs(userGreeterCacheDir(GreeterCacheDir, username))
if err != nil {
return false
}
abs, err := filepath.Abs(path)
if err != nil {
return false
}
return abs == userDir || strings.HasPrefix(abs, userDir+string(filepath.Separator))
}
func UserIsInGreeterGroup(username string) bool {
group := DetectGreeterGroup()
if !utils.HasGroup(group) {
return false
}
groupsCmd := exec.Command("groups", username)
groupsOutput, err := groupsCmd.Output()
if err != nil {
return false
}
return strings.Contains(string(groupsOutput), group)
}
func CanSyncOwnUserGreeterProfile(username string) bool {
currentUser, err := user.Current()
if err != nil || currentUser.Username != username {
return false
}
if !UserIsInGreeterGroup(username) {
return false
}
usersDir := filepath.Join(GreeterCacheDir, "users")
if st, err := os.Stat(usersDir); err != nil || !st.IsDir() {
return false
}
testFile := filepath.Join(usersDir, ".write-test-"+username)
file, err := os.OpenFile(testFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o660)
if err != nil {
return false
}
_ = file.Close()
_ = os.Remove(testFile)
return true
}
func GreeterProfileSyncReady() bool {
if command := readGreeterSessionCommand(); command != "" && strings.Contains(command, "dms-greeter") {
return true
}
usersDir := filepath.Join(GreeterCacheDir, "users")
st, err := os.Stat(usersDir)
return err == nil && st.IsDir()
}
func readGreeterSessionCommand() string {
data, err := os.ReadFile("/etc/greetd/config.toml")
if err != nil {
return ""
}
inDefaultSession := false
for line := range strings.SplitSeq(string(data), "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
inDefaultSession = strings.EqualFold(strings.Trim(trimmed, "[]"), "default_session")
continue
}
if !inDefaultSession {
continue
}
if idx := strings.Index(trimmed, "#"); idx >= 0 {
trimmed = strings.TrimSpace(trimmed[:idx])
}
if !strings.HasPrefix(trimmed, "command") {
continue
}
parts := strings.SplitN(trimmed, "=", 2)
if len(parts) != 2 {
continue
}
command := strings.Trim(strings.TrimSpace(parts[1]), `"`)
if command != "" {
return command
}
}
return ""
}
// SyncUserProfileCache writes the current user's theme slot under users/<username>/
// without modifying greetd or other system configuration. Requires membership in the
// greeter group and a prior full greeter setup by an administrator.
func SyncUserProfileCache(logFunc func(string)) error {
if logFunc == nil {
logFunc = func(string) {}
}
if !GreeterProfileSyncReady() {
return fmt.Errorf("greeter is not set up on this system yet; an administrator must run 'dms greeter install' or 'dms greeter sync' once first")
}
currentUser, err := user.Current()
if err != nil {
return fmt.Errorf("failed to resolve current user: %w", err)
}
if !CanSyncOwnUserGreeterProfile(currentUser.Username) {
group := DetectGreeterGroup()
return fmt.Errorf("cannot sync greeter profile: you must be in the %s group with write access to %s/users\nAsk an administrator to run:\n sudo usermod -aG %s %s\nThen log out and back in before running:\n dms greeter sync --profile",
group, GreeterCacheDir, group, currentUser.Username)
}
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err)
}
state, err := resolveGreeterThemeSyncState(homeDir)
if err != nil {
return fmt.Errorf("failed to resolve greeter color source: %w", err)
}
if err := syncUserGreeterCacheSlot(homeDir, GreeterCacheDir, currentUser.Username, state, logFunc, userSlotSyncOpts{
profileOnly: true,
}); err != nil {
return err
}
logFunc(fmt.Sprintf(" → %s/users/%s/", GreeterCacheDir, currentUser.Username))
return nil
}
func canWriteUserGreeterCacheSlot(dest, username string) bool {
return isUserOwnedGreeterCacheSlot(dest, username) && CanSyncOwnUserGreeterProfile(username)
}
type userSlotSyncOpts struct {
sudoPassword string
profileOnly bool
username string
}
func (o userSlotSyncOpts) useDirectWrite(dest string) bool {
if !o.profileOnly {
return false
}
return canWriteUserGreeterCacheSlot(dest, o.username)
}
func isGreeterCachePath(path string) bool {
abs, err := filepath.Abs(path)
if err != nil {
return true
}
cacheAbs, err := filepath.Abs(GreeterCacheDir)
if err != nil {
return true
}
if abs == cacheAbs {
return true
}
return strings.HasPrefix(abs, cacheAbs+string(filepath.Separator))
}
func greeterCacheOwner() string {
greeterGroup := DetectGreeterGroup()
daemonUser := DetectGreeterUser()
return daemonUser + ":" + greeterGroup
}
func ensureGreeterCacheSubdir(dir string, opts userSlotSyncOpts) error {
if opts.useDirectWrite(dir) {
if err := os.MkdirAll(dir, 0o770); err != nil {
return fmt.Errorf("failed to create cache directory %s: %w", dir, err)
}
return nil
}
if err := privesc.Run(context.Background(), opts.sudoPassword, "mkdir", "-p", dir); err != nil {
return fmt.Errorf("failed to create cache directory %s: %w", dir, err)
}
owner := greeterCacheOwner()
if err := privesc.Run(context.Background(), opts.sudoPassword, "chown", owner, dir); err != nil {
if fallbackErr := privesc.Run(context.Background(), opts.sudoPassword, "chown", "root:"+DetectGreeterGroup(), dir); fallbackErr != nil {
return fmt.Errorf("failed to set ownership on %s: %w", dir, err)
}
}
if err := privesc.Run(context.Background(), opts.sudoPassword, "chmod", "2770", dir); err != nil {
return fmt.Errorf("failed to set permissions on %s: %w", dir, err)
}
return nil
}
func setGreeterCacheFileOwnership(path, sudoPassword string) error {
owner := greeterCacheOwner()
if err := privesc.Run(context.Background(), sudoPassword, "chown", owner, path); err != nil {
if fallbackErr := privesc.Run(context.Background(), sudoPassword, "chown", "root:"+DetectGreeterGroup(), path); fallbackErr != nil {
return fmt.Errorf("failed to set ownership on %s: %w", path, err)
}
}
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", path); err != nil {
return fmt.Errorf("failed to set permissions on %s: %w", path, err)
}
return nil
}
func syncUserGreeterCacheSlot(homeDir, cacheDir, username string, state greeterThemeSyncState, logFunc func(string), opts userSlotSyncOpts) error {
if strings.TrimSpace(username) == "" {
return nil
}
opts.username = username
userDir := userGreeterCacheDir(cacheDir, username)
if err := ensureGreeterCacheSubdir(userDir, opts); err != nil {
return err
}
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
settingsBytes, err := os.ReadFile(settingsPath)
if err != nil {
return fmt.Errorf("failed to read settings for user cache slot: %w", err)
}
settingsMap := map[string]any{}
if strings.TrimSpace(string(settingsBytes)) != "" {
if err := json.Unmarshal(settingsBytes, &settingsMap); err != nil {
return fmt.Errorf("failed to parse settings for user cache slot: %w", err)
}
}
if customTheme, ok := settingsMap["customThemeFile"].(string); ok && strings.TrimSpace(customTheme) != "" {
resolvedTheme := customTheme
if !filepath.IsAbs(resolvedTheme) {
resolvedTheme = filepath.Join(homeDir, resolvedTheme)
}
if st, statErr := os.Stat(resolvedTheme); statErr == nil && !st.IsDir() {
destTheme := filepath.Join(userDir, "custom-theme.json")
if err := copyFileWithPrivesc(resolvedTheme, destTheme, opts); err != nil {
return err
}
settingsMap["customThemeFile"] = destTheme
}
}
settingsBytes, err = json.Marshal(settingsMap)
if err != nil {
return fmt.Errorf("failed to marshal settings for user cache slot: %w", err)
}
if err := writeFileWithPrivesc(filepath.Join(userDir, "settings.json"), settingsBytes, opts); err != nil {
return err
}
sessionPath := filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json")
sessionBytes, err := os.ReadFile(sessionPath)
if err != nil {
return fmt.Errorf("failed to read session for user cache slot: %w", err)
}
sessionMap := map[string]any{}
if strings.TrimSpace(string(sessionBytes)) != "" {
if err := json.Unmarshal(sessionBytes, &sessionMap); err != nil {
return fmt.Errorf("failed to parse session for user cache slot: %w", err)
}
}
if err := localizeSessionWallpapers(sessionMap, userDir, opts); err != nil {
return err
}
sessionBytes, err = json.Marshal(sessionMap)
if err != nil {
return fmt.Errorf("failed to marshal session for user cache slot: %w", err)
}
if err := writeFileWithPrivesc(filepath.Join(userDir, "session.json"), sessionBytes, opts); err != nil {
return err
}
colorsSource := state.effectiveColorsSource(homeDir)
if err := copyFileWithPrivesc(colorsSource, filepath.Join(userDir, "colors.json"), opts); err != nil {
return fmt.Errorf("failed to copy colors for user cache slot: %w", err)
}
if err := syncUserProfileImage(homeDir, userDir, opts); err != nil {
return err
}
rootOverride := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
userOverride := filepath.Join(userDir, "greeter_wallpaper_override.jpg")
if st, statErr := os.Stat(rootOverride); statErr == nil && !st.IsDir() {
if err := copyFileWithPrivesc(rootOverride, userOverride, opts); err != nil {
return fmt.Errorf("failed to copy greeter wallpaper override for user cache slot: %w", err)
}
} else if opts.useDirectWrite(userOverride) {
_ = os.Remove(userOverride)
} else {
_ = privesc.Run(context.Background(), opts.sudoPassword, "rm", "-f", userOverride)
}
logFunc(fmt.Sprintf("✓ Synced per-user greeter cache for %s", username))
return nil
}
func localizeSessionWallpapers(session map[string]any, userDir string, opts userSlotSyncOpts) error {
stringKeys := []struct {
key string
prefix string
}{
{"wallpaperPath", "wallpaper"},
{"wallpaperPathLight", "wallpaper-light"},
{"wallpaperPathDark", "wallpaper-dark"},
}
for _, item := range stringKeys {
if err := localizeWallpaperStringField(session, item.key, userDir, item.prefix, opts); err != nil {
return err
}
}
mapKeys := []struct {
key string
prefix string
}{
{"monitorWallpapers", "wallpaper-monitor"},
{"monitorWallpapersLight", "wallpaper-monitor-light"},
{"monitorWallpapersDark", "wallpaper-monitor-dark"},
}
for _, item := range mapKeys {
if err := localizeWallpaperMapField(session, item.key, userDir, item.prefix, opts); err != nil {
return err
}
}
return nil
}
func localizeWallpaperStringField(session map[string]any, key, userDir, prefix string, opts userSlotSyncOpts) error {
raw, ok := session[key]
if !ok {
return nil
}
path, ok := raw.(string)
if !ok || strings.TrimSpace(path) == "" {
return nil
}
dest, err := copyWallpaperIntoUserCache(path, userDir, prefix, opts)
if err != nil {
return err
}
if dest != "" {
session[key] = dest
}
return nil
}
func localizeWallpaperMapField(session map[string]any, key, userDir, prefix string, opts userSlotSyncOpts) error {
raw, ok := session[key]
if !ok || raw == nil {
return nil
}
values, ok := raw.(map[string]any)
if !ok {
return nil
}
for monitor, rawPath := range values {
path, ok := rawPath.(string)
if !ok || strings.TrimSpace(path) == "" {
continue
}
safeMonitor := monitorWallpaperSanitizer.ReplaceAllString(monitor, "-")
dest, err := copyWallpaperIntoUserCache(path, userDir, prefix+"-"+safeMonitor, opts)
if err != nil {
return err
}
if dest != "" {
values[monitor] = dest
}
}
return nil
}
func copyWallpaperIntoUserCache(srcPath, userDir, prefix string, opts userSlotSyncOpts) (string, error) {
if strings.TrimSpace(srcPath) == "" {
return "", nil
}
st, err := os.Stat(srcPath)
if err != nil || st.IsDir() {
return "", nil
}
ext := filepath.Ext(srcPath)
if ext == "" {
ext = ".jpg"
}
dest := filepath.Join(userDir, prefix+ext)
if err := copyFileWithPrivesc(srcPath, dest, opts); err != nil {
return "", err
}
return dest, nil
}
func copyFileWithPrivesc(src, dest string, opts userSlotSyncOpts) error {
if opts.useDirectWrite(dest) {
if err := os.MkdirAll(filepath.Dir(dest), 0o770); err != nil {
return fmt.Errorf("failed to create parent dir for %s: %w", dest, err)
}
data, err := os.ReadFile(src)
if err != nil {
return fmt.Errorf("failed to read %s: %w", src, err)
}
if err := os.WriteFile(dest, data, 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", dest, err)
}
return nil
}
if !isGreeterCachePath(dest) {
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
return fmt.Errorf("failed to create parent dir for %s: %w", dest, err)
}
data, err := os.ReadFile(src)
if err != nil {
return fmt.Errorf("failed to read %s: %w", src, err)
}
if err := os.WriteFile(dest, data, 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", dest, err)
}
return nil
}
_ = privesc.Run(context.Background(), opts.sudoPassword, "rm", "-f", dest)
if err := privesc.Run(context.Background(), opts.sudoPassword, "cp", src, dest); err != nil {
return fmt.Errorf("failed to copy %s to %s: %w", src, dest, err)
}
return setGreeterCacheFileOwnership(dest, opts.sudoPassword)
}
func writeFileWithPrivesc(path string, data []byte, opts userSlotSyncOpts) error {
if opts.useDirectWrite(path) {
if err := os.MkdirAll(filepath.Dir(path), 0o770); err != nil {
return fmt.Errorf("failed to create parent dir for %s: %w", path, err)
}
if err := os.WriteFile(path, data, 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", path, err)
}
return nil
}
if !isGreeterCachePath(path) {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return fmt.Errorf("failed to create parent dir for %s: %w", path, err)
}
if err := os.WriteFile(path, data, 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", path, err)
}
return nil
}
tmp, err := os.CreateTemp("", "dms-greeter-user-cache-*")
if err != nil {
return fmt.Errorf("failed to create temp file for %s: %w", path, err)
}
tmpPath := tmp.Name()
if _, err := tmp.Write(data); err != nil {
_ = tmp.Close()
_ = os.Remove(tmpPath)
return fmt.Errorf("failed to write temp file for %s: %w", path, err)
}
if err := tmp.Close(); err != nil {
_ = os.Remove(tmpPath)
return fmt.Errorf("failed to close temp file for %s: %w", path, err)
}
defer os.Remove(tmpPath)
_ = privesc.Run(context.Background(), opts.sudoPassword, "rm", "-f", path)
if err := privesc.Run(context.Background(), opts.sudoPassword, "cp", tmpPath, path); err != nil {
return fmt.Errorf("failed to install %s: %w", path, err)
}
return setGreeterCacheFileOwnership(path, opts.sudoPassword)
}
func resolveUserProfileImageSource(homeDir string) string {
candidates := []string{
filepath.Join(homeDir, ".face"),
filepath.Join(homeDir, ".face.icon"),
}
if homeDir != "" {
username := filepath.Base(homeDir)
if username != "" && username != "." && username != string(filepath.Separator) {
candidates = append([]string{filepath.Join("/var/lib/AccountsService/icons", username)}, candidates...)
}
}
for _, src := range candidates {
st, err := os.Stat(src)
if err == nil && !st.IsDir() && st.Size() > 0 {
return src
}
}
return ""
}
func syncUserProfileImage(homeDir, userDir string, opts userSlotSyncOpts) error {
for _, name := range []string{"profile.jpg", "profile.jpeg", "profile.png", "profile.webp"} {
path := filepath.Join(userDir, name)
if opts.useDirectWrite(path) {
_ = os.Remove(path)
} else {
_ = privesc.Run(context.Background(), opts.sudoPassword, "rm", "-f", path)
}
}
src := resolveUserProfileImageSource(homeDir)
if src == "" {
return nil
}
ext := filepath.Ext(src)
if ext == "" {
ext = ".jpg"
}
dest := filepath.Join(userDir, "profile"+ext)
if err := copyFileWithPrivesc(src, dest, opts); err != nil {
return fmt.Errorf("failed to copy profile image for user cache slot: %w", err)
}
return nil
}
@@ -1,81 +0,0 @@
package greeter
import (
"path/filepath"
"testing"
)
func TestUserGreeterCacheDir(t *testing.T) {
t.Parallel()
got := userGreeterCacheDir("/var/cache/dms-greeter", "alice")
want := filepath.Join("/var/cache/dms-greeter", "users", "alice")
if got != want {
t.Fatalf("userGreeterCacheDir() = %q, want %q", got, want)
}
}
func TestResolveUserProfileImageSource(t *testing.T) {
t.Parallel()
homeDir := t.TempDir()
facePath := filepath.Join(homeDir, ".face")
writeTestFile(t, facePath, "face")
got := resolveUserProfileImageSource(homeDir)
if got != facePath {
t.Fatalf("resolveUserProfileImageSource() = %q, want %q", got, facePath)
}
}
func TestIsUserOwnedGreeterCacheSlot(t *testing.T) {
t.Parallel()
slot := filepath.Join(GreeterCacheDir, "users", "alice", "settings.json")
if !isUserOwnedGreeterCacheSlot(slot, "alice") {
t.Fatalf("expected alice to own %q", slot)
}
if isUserOwnedGreeterCacheSlot(slot, "bob") {
t.Fatalf("expected bob not to own alice slot")
}
if isUserOwnedGreeterCacheSlot(filepath.Join(GreeterCacheDir, "settings.json"), "alice") {
t.Fatalf("expected root cache file not to be a user slot")
}
}
func TestLocalizeSessionWallpapers(t *testing.T) {
t.Parallel()
homeDir := t.TempDir()
userDir := filepath.Join(homeDir, "users", "alice")
wallpaperPath := filepath.Join(homeDir, "wall.jpg")
writeTestFile(t, wallpaperPath, "wallpaper")
session := map[string]any{
"wallpaperPath": wallpaperPath,
"monitorWallpapers": map[string]any{
"DP-1": wallpaperPath,
},
}
if err := localizeSessionWallpapers(session, userDir, userSlotSyncOpts{}); err != nil {
t.Fatalf("localizeSessionWallpapers returned error: %v", err)
}
gotPath, ok := session["wallpaperPath"].(string)
if !ok || gotPath == "" {
t.Fatalf("expected localized wallpaperPath, got %#v", session["wallpaperPath"])
}
if gotPath == wallpaperPath {
t.Fatalf("expected copied wallpaper path, still points to source")
}
monitorMap, ok := session["monitorWallpapers"].(map[string]any)
if !ok {
t.Fatalf("expected monitorWallpapers map")
}
monitorPath, ok := monitorMap["DP-1"].(string)
if !ok || monitorPath == "" || monitorPath == wallpaperPath {
t.Fatalf("expected localized monitor wallpaper, got %#v", monitorMap["DP-1"])
}
}
+2 -6
View File
@@ -9,8 +9,8 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc" "github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
// ErrConfirmationRequired is returned when --yes is not set and the user // ErrConfirmationRequired is returned when --yes is not set and the user
@@ -223,16 +223,12 @@ func (r *Runner) Run() error {
// 9. Greeter setup (if dms-greeter was included) // 9. Greeter setup (if dms-greeter was included)
if !disabledItems["dms-greeter"] && r.depExists(dependencies, "dms-greeter") { if !disabledItems["dms-greeter"] && r.depExists(dependencies, "dms-greeter") {
compositorName := "niri"
if wm == deps.WindowManagerHyprland {
compositorName = "Hyprland"
}
fmt.Fprintln(os.Stdout, "Configuring DMS greeter...") fmt.Fprintln(os.Stdout, "Configuring DMS greeter...")
logFunc := func(line string) { logFunc := func(line string) {
r.log(line) r.log(line)
fmt.Fprintf(os.Stdout, " greeter: %s\n", line) fmt.Fprintf(os.Stdout, " greeter: %s\n", line)
} }
if err := greeter.AutoSetupGreeter(compositorName, sudoPassword, logFunc); err != nil { if err := utils.RunDmsGreeterInstall(sudoPassword, logFunc); err != nil {
// Non-fatal, matching TUI behavior // Non-fatal, matching TUI behavior
fmt.Fprintf(os.Stderr, "Warning: greeter setup issue (non-fatal): %v\n", err) fmt.Fprintf(os.Stderr, "Warning: greeter setup issue (non-fatal): %v\n", err)
} }
-379
View File
@@ -5,30 +5,20 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc" "github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils" "github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
const ( const (
GreeterPamManagedBlockStart = "# BEGIN DMS GREETER AUTH (managed by dms greeter sync)"
GreeterPamManagedBlockEnd = "# END DMS GREETER AUTH"
LockscreenPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN AUTH (managed by dms greeter sync)" LockscreenPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN AUTH (managed by dms greeter sync)"
LockscreenPamManagedBlockEnd = "# END DMS LOCKSCREEN AUTH" LockscreenPamManagedBlockEnd = "# END DMS LOCKSCREEN AUTH"
LockscreenU2FPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN U2F AUTH (managed by dms auth sync)" LockscreenU2FPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN U2F AUTH (managed by dms auth sync)"
LockscreenU2FPamManagedBlockEnd = "# END DMS LOCKSCREEN U2F AUTH" LockscreenU2FPamManagedBlockEnd = "# END DMS LOCKSCREEN U2F AUTH"
legacyGreeterPamFprintComment = "# DMS greeter fingerprint"
legacyGreeterPamU2FComment = "# DMS greeter U2F"
GreetdPamPath = "/etc/pam.d/greetd"
DankshellPamPath = "/etc/pam.d/dankshell" DankshellPamPath = "/etc/pam.d/dankshell"
DankshellU2FPamPath = "/etc/pam.d/dankshell-u2f" DankshellU2FPamPath = "/etc/pam.d/dankshell-u2f"
) )
@@ -76,19 +66,14 @@ var includedPamAuthFiles = []string{
type AuthSettings struct { type AuthSettings struct {
EnableFprint bool `json:"enableFprint"` EnableFprint bool `json:"enableFprint"`
EnableU2f bool `json:"enableU2f"` EnableU2f bool `json:"enableU2f"`
GreeterEnableFprint bool `json:"greeterEnableFprint"`
GreeterEnableU2f bool `json:"greeterEnableU2f"`
GreeterPamExternallyManaged bool `json:"greeterPamExternallyManaged"`
} }
type SyncAuthOptions struct { type SyncAuthOptions struct {
HomeDir string HomeDir string
ForceGreeterAuth bool
} }
type syncDeps struct { type syncDeps struct {
pamDir string pamDir string
greetdPath string
dankshellPath string dankshellPath string
dankshellU2fPath string dankshellU2fPath string
isNixOS func() bool isNixOS func() bool
@@ -97,8 +82,6 @@ type syncDeps struct {
createTemp func(string, string) (*os.File, error) createTemp func(string, string) (*os.File, error)
removeFile func(string) error removeFile func(string) error
runSudoCmd func(string, string, ...string) error runSudoCmd func(string, string, ...string) error
pamModuleExists func(string) bool
fingerprintAvailableForCurrentUser func() bool
} }
type lockscreenPamIncludeDirective struct { type lockscreenPamIncludeDirective struct {
@@ -154,7 +137,6 @@ func (r lockscreenPamResolver) locate(target string) (string, error) {
func defaultSyncDeps() syncDeps { func defaultSyncDeps() syncDeps {
return syncDeps{ return syncDeps{
pamDir: "/etc/pam.d", pamDir: "/etc/pam.d",
greetdPath: GreetdPamPath,
dankshellPath: DankshellPamPath, dankshellPath: DankshellPamPath,
dankshellU2fPath: DankshellU2FPamPath, dankshellU2fPath: DankshellU2FPamPath,
isNixOS: IsNixOS, isNixOS: IsNixOS,
@@ -165,8 +147,6 @@ func defaultSyncDeps() syncDeps {
runSudoCmd: func(password, command string, args ...string) error { runSudoCmd: func(password, command string, args ...string) error {
return privesc.Run(context.Background(), password, append([]string{command}, args...)...) return privesc.Run(context.Background(), password, append([]string{command}, args...)...)
}, },
pamModuleExists: pamModuleExists,
fingerprintAvailableForCurrentUser: FingerprintAuthAvailableForCurrentUser,
} }
} }
@@ -195,22 +175,10 @@ func ReadAuthSettings(homeDir string) (AuthSettings, error) {
return settings, nil return settings, nil
} }
func ReadGreeterAuthToggles(homeDir string) (enableFprint bool, enableU2f bool, err error) {
settings, err := ReadAuthSettings(homeDir)
if err != nil {
return false, false, err
}
return settings.GreeterEnableFprint, settings.GreeterEnableU2f, nil
}
func SyncAuthConfig(logFunc func(string), sudoPassword string, options SyncAuthOptions) error { func SyncAuthConfig(logFunc func(string), sudoPassword string, options SyncAuthOptions) error {
return syncAuthConfigWithDeps(logFunc, sudoPassword, options, defaultSyncDeps()) return syncAuthConfigWithDeps(logFunc, sudoPassword, options, defaultSyncDeps())
} }
func RemoveManagedGreeterPamBlock(logFunc func(string), sudoPassword string) error {
return removeManagedGreeterPamBlockWithDeps(logFunc, sudoPassword, defaultSyncDeps())
}
func syncAuthConfigWithDeps(logFunc func(string), sudoPassword string, options SyncAuthOptions, deps syncDeps) error { func syncAuthConfigWithDeps(logFunc func(string), sudoPassword string, options SyncAuthOptions, deps syncDeps) error {
homeDir := strings.TrimSpace(options.HomeDir) homeDir := strings.TrimSpace(options.HomeDir)
if homeDir == "" { if homeDir == "" {
@@ -233,97 +201,7 @@ func syncAuthConfigWithDeps(logFunc func(string), sudoPassword string, options S
return err return err
} }
if _, err := deps.stat(deps.greetdPath); err != nil {
if os.IsNotExist(err) {
logFunc(" /etc/pam.d/greetd not found. Skipping greeter PAM sync.")
return nil return nil
}
return fmt.Errorf("failed to inspect %s: %w", deps.greetdPath, err)
}
if settings.GreeterPamExternallyManaged {
if err := removeManagedGreeterPamBlockWithDeps(logFunc, sudoPassword, deps); err != nil {
return err
}
logFunc(" /etc/pam.d/greetd is externally managed. Skipping DMS greeter PAM sync.")
return nil
}
if err := syncGreeterPamConfigWithDeps(logFunc, sudoPassword, settings, options.ForceGreeterAuth, deps); err != nil {
return err
}
return nil
}
func removeManagedGreeterPamBlockWithDeps(logFunc func(string), sudoPassword string, deps syncDeps) error {
if deps.isNixOS() {
return nil
}
data, err := deps.readFile(deps.greetdPath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("failed to read %s: %w", deps.greetdPath, err)
}
originalContent := string(data)
stripped, removed := stripManagedGreeterPamBlock(originalContent)
strippedAgain, removedLegacy := stripLegacyGreeterPamLines(stripped)
if !removed && !removedLegacy {
return nil
}
if err := writeManagedPamFile(strippedAgain, deps.greetdPath, sudoPassword, deps); err != nil {
return fmt.Errorf("failed to write %s: %w", deps.greetdPath, err)
}
logFunc("✓ Removed DMS managed PAM block from " + deps.greetdPath)
return nil
}
func ParseManagedGreeterPamAuth(pamText string) (managed bool, fingerprint bool, u2f bool, legacy bool) {
if pamText == "" {
return false, false, false, false
}
lines := strings.Split(pamText, "\n")
inManaged := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
switch trimmed {
case GreeterPamManagedBlockStart:
managed = true
inManaged = true
continue
case GreeterPamManagedBlockEnd:
inManaged = false
continue
}
if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) {
legacy = true
}
if !inManaged {
continue
}
if strings.Contains(trimmed, "pam_fprintd") {
fingerprint = true
}
if strings.Contains(trimmed, "pam_u2f") {
u2f = true
}
}
return managed, fingerprint, u2f, legacy
}
func StripManagedGreeterPamContent(pamText string) (string, bool) {
stripped, removed := stripManagedGreeterPamBlock(pamText)
stripped, removedLegacy := stripLegacyGreeterPamLines(stripped)
return stripped, removed || removedLegacy
} }
func PamTextIncludesFile(pamText, filename string) bool { func PamTextIncludesFile(pamText, filename string) bool {
@@ -1034,186 +912,6 @@ func syncLockscreenU2FPamConfigWithDeps(logFunc func(string), sudoPassword strin
return nil return nil
} }
func stripManagedGreeterPamBlock(content string) (string, bool) {
lines := strings.Split(content, "\n")
filtered := make([]string, 0, len(lines))
inManagedBlock := false
removed := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == GreeterPamManagedBlockStart {
inManagedBlock = true
removed = true
continue
}
if trimmed == GreeterPamManagedBlockEnd {
inManagedBlock = false
removed = true
continue
}
if inManagedBlock {
removed = true
continue
}
filtered = append(filtered, line)
}
return strings.Join(filtered, "\n"), removed
}
func stripLegacyGreeterPamLines(content string) (string, bool) {
lines := strings.Split(content, "\n")
filtered := make([]string, 0, len(lines))
removed := false
for i := 0; i < len(lines); i++ {
trimmed := strings.TrimSpace(lines[i])
if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) {
removed = true
if i+1 < len(lines) {
nextLine := strings.TrimSpace(lines[i+1])
if strings.HasPrefix(nextLine, "auth") &&
(strings.Contains(nextLine, "pam_fprintd") || strings.Contains(nextLine, "pam_u2f")) {
i++
}
}
continue
}
filtered = append(filtered, lines[i])
}
return strings.Join(filtered, "\n"), removed
}
func insertManagedGreeterPamBlock(content string, blockLines []string, greetdPamPath string) (string, error) {
lines := strings.Split(content, "\n")
for i, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed != "" && !strings.HasPrefix(trimmed, "#") && strings.HasPrefix(trimmed, "auth") {
block := strings.Join(blockLines, "\n")
prefix := strings.Join(lines[:i], "\n")
suffix := strings.Join(lines[i:], "\n")
switch {
case prefix == "":
return block + "\n" + suffix, nil
case suffix == "":
return prefix + "\n" + block, nil
default:
return prefix + "\n" + block + "\n" + suffix, nil
}
}
}
return "", fmt.Errorf("no auth directive found in %s", greetdPamPath)
}
func syncGreeterPamConfigWithDeps(logFunc func(string), sudoPassword string, settings AuthSettings, forceAuth bool, deps syncDeps) error {
var wantFprint, wantU2f bool
fprintToggleEnabled := forceAuth
u2fToggleEnabled := forceAuth
if forceAuth {
wantFprint = deps.pamModuleExists("pam_fprintd.so")
wantU2f = deps.pamModuleExists("pam_u2f.so")
} else {
fprintToggleEnabled = settings.GreeterEnableFprint
u2fToggleEnabled = settings.GreeterEnableU2f
fprintModule := deps.pamModuleExists("pam_fprintd.so")
u2fModule := deps.pamModuleExists("pam_u2f.so")
wantFprint = settings.GreeterEnableFprint && fprintModule
wantU2f = settings.GreeterEnableU2f && u2fModule
if settings.GreeterEnableFprint && !fprintModule {
logFunc("⚠ Warning: greeter fingerprint toggle is enabled, but pam_fprintd.so was not found.")
}
if settings.GreeterEnableU2f && !u2fModule {
logFunc("⚠ Warning: greeter security key toggle is enabled, but pam_u2f.so was not found.")
}
}
if deps.isNixOS() {
logFunc(" NixOS detected: PAM config is managed by NixOS modules. Skipping DMS PAM block write.")
logFunc(" Configure fingerprint/U2F auth via your greetd NixOS module options (e.g. security.pam.services.greetd).")
return nil
}
pamData, err := deps.readFile(deps.greetdPath)
if err != nil {
return fmt.Errorf("failed to read %s: %w", deps.greetdPath, err)
}
originalContent := string(pamData)
content, _ := stripManagedGreeterPamBlock(originalContent)
content, _ = stripLegacyGreeterPamLines(content)
includedFprintFile := detectIncludedPamModule(content, "pam_fprintd.so", deps)
includedU2fFile := detectIncludedPamModule(content, "pam_u2f.so", deps)
fprintAvailableForCurrentUser := deps.fingerprintAvailableForCurrentUser()
if wantFprint && includedFprintFile != "" {
logFunc("⚠ pam_fprintd already present in included " + includedFprintFile + " (managed by authselect/pam-auth-update). Skipping DMS fprint block to avoid double-fingerprint auth.")
wantFprint = false
}
if wantU2f && includedU2fFile != "" {
logFunc("⚠ pam_u2f already present in included " + includedU2fFile + " (managed by authselect/pam-auth-update). Skipping DMS U2F block to avoid double security-key auth.")
wantU2f = false
}
if !wantFprint && includedFprintFile != "" {
if fprintToggleEnabled {
logFunc(" Fingerprint auth is still enabled via included " + includedFprintFile + ".")
if fprintAvailableForCurrentUser {
logFunc(" DMS toggle is enabled, and effective auth is provided by the included PAM stack.")
} else {
logFunc(" No enrolled fingerprints detected for the current user; password auth remains the effective path.")
}
} else {
if fprintAvailableForCurrentUser {
logFunc(" Fingerprint auth is active via included " + includedFprintFile + " while DMS fingerprint toggle is off.")
logFunc(" Password login will work but may be delayed while the fingerprint module runs first.")
logFunc(" To eliminate the delay, " + pamManagerHintForCurrentDistro())
} else {
logFunc(" pam_fprintd is present via included " + includedFprintFile + ", but no enrolled fingerprints were detected for the current user.")
logFunc(" Password auth remains the effective login path.")
}
}
}
if !wantU2f && includedU2fFile != "" {
if u2fToggleEnabled {
logFunc(" Security-key auth is still enabled via included " + includedU2fFile + ".")
logFunc(" DMS toggle is enabled, but effective auth is provided by the included PAM stack.")
} else {
logFunc("⚠ Security-key auth is active via included " + includedU2fFile + " while DMS security-key toggle is off.")
logFunc(" " + pamManagerHintForCurrentDistro())
}
}
if wantFprint || wantU2f {
blockLines := []string{GreeterPamManagedBlockStart}
if wantFprint {
blockLines = append(blockLines, "auth sufficient pam_fprintd.so max-tries=2 timeout=10")
}
if wantU2f {
blockLines = append(blockLines, "auth sufficient pam_u2f.so cue nouserok timeout=10")
}
blockLines = append(blockLines, GreeterPamManagedBlockEnd)
content, err = insertManagedGreeterPamBlock(content, blockLines, deps.greetdPath)
if err != nil {
return err
}
}
if content == originalContent {
return nil
}
if err := writeManagedPamFile(content, deps.greetdPath, sudoPassword, deps); err != nil {
return fmt.Errorf("failed to install updated PAM config at %s: %w", deps.greetdPath, err)
}
if wantFprint || wantU2f {
logFunc("✓ Configured greetd PAM for fingerprint/U2F")
} else {
logFunc("✓ Cleared DMS-managed greeter PAM auth block")
}
return nil
}
func writeManagedPamFile(content string, destPath string, sudoPassword string, deps syncDeps) error { func writeManagedPamFile(content string, destPath string, sudoPassword string, deps syncDeps) error {
tmpFile, err := deps.createTemp("", "dms-pam-*.conf") tmpFile, err := deps.createTemp("", "dms-pam-*.conf")
if err != nil { if err != nil {
@@ -1240,26 +938,6 @@ func writeManagedPamFile(content string, destPath string, sudoPassword string, d
return nil return nil
} }
func pamManagerHintForCurrentDistro() string {
osInfo, err := distros.GetOSInfo()
if err != nil {
return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
}
config, exists := distros.Registry[osInfo.Distribution.ID]
if !exists {
return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
}
switch config.Family {
case distros.FamilyFedora:
return "Disable it in authselect to force password-only greeter login."
case distros.FamilyDebian, distros.FamilyUbuntu:
return "Disable it in pam-auth-update to force password-only greeter login."
default:
return "Disable it in your distro PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
}
}
func pamModuleExists(module string) bool { func pamModuleExists(module string) bool {
for _, libDir := range []string{ for _, libDir := range []string{
"/usr/lib64/security", "/usr/lib64/security",
@@ -1282,60 +960,3 @@ func pamModuleExists(module string) bool {
} }
return false return false
} }
func hasEnrolledFingerprintOutput(output string) bool {
lower := strings.ToLower(output)
if strings.Contains(lower, "no fingers enrolled") ||
strings.Contains(lower, "no fingerprints enrolled") ||
strings.Contains(lower, "no prints enrolled") {
return false
}
if strings.Contains(lower, "has fingers enrolled") ||
strings.Contains(lower, "has fingerprints enrolled") {
return true
}
for _, line := range strings.Split(lower, "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "finger:") {
return true
}
if strings.HasPrefix(trimmed, "- ") && strings.Contains(trimmed, "finger") {
return true
}
}
return false
}
func FingerprintAuthAvailableForCurrentUser() bool {
username := strings.TrimSpace(os.Getenv("SUDO_USER"))
if username == "" {
username = strings.TrimSpace(os.Getenv("USER"))
}
if username == "" {
out, err := exec.Command("id", "-un").Output()
if err == nil {
username = strings.TrimSpace(string(out))
}
}
return fingerprintAuthAvailableForUser(username)
}
func fingerprintAuthAvailableForUser(username string) bool {
username = strings.TrimSpace(username)
if username == "" {
return false
}
if !pamModuleExists("pam_fprintd.so") {
return false
}
if _, err := exec.LookPath("fprintd-list"); err != nil {
return false
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
out, err := exec.CommandContext(ctx, "fprintd-list", username).CombinedOutput()
if err != nil {
return false
}
return hasEnrolledFingerprintOutput(string(out))
}
+94 -271
View File
@@ -18,42 +18,6 @@ func writeTestFile(t *testing.T, path string, content string) {
} }
} }
type pamTestEnv struct {
pamDir string
greetdPath string
dankshellPath string
dankshellU2fPath string
tmpDir string
homeDir string
availableModules map[string]bool
fingerprintAvailable bool
}
func newPamTestEnv(t *testing.T) *pamTestEnv {
t.Helper()
root := t.TempDir()
pamDir := filepath.Join(root, "pam.d")
tmpDir := filepath.Join(root, "tmp")
homeDir := filepath.Join(root, "home")
for _, dir := range []string{pamDir, tmpDir, homeDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("failed to create %s: %v", dir, err)
}
}
return &pamTestEnv{
pamDir: pamDir,
greetdPath: filepath.Join(pamDir, "greetd"),
dankshellPath: filepath.Join(pamDir, "dankshell"),
dankshellU2fPath: filepath.Join(pamDir, "dankshell-u2f"),
tmpDir: tmpDir,
homeDir: homeDir,
availableModules: map[string]bool{},
}
}
func (e *pamTestEnv) writePamFile(t *testing.T, name string, content string) { func (e *pamTestEnv) writePamFile(t *testing.T, name string, content string) {
t.Helper() t.Helper()
writeTestFile(t, filepath.Join(e.pamDir, name), content) writeTestFile(t, filepath.Join(e.pamDir, name), content)
@@ -64,59 +28,6 @@ func (e *pamTestEnv) writeSettings(t *testing.T, content string) {
writeTestFile(t, filepath.Join(e.homeDir, ".config", "DankMaterialShell", "settings.json"), content) writeTestFile(t, filepath.Join(e.homeDir, ".config", "DankMaterialShell", "settings.json"), content)
} }
func (e *pamTestEnv) deps(isNixOS bool) syncDeps {
return syncDeps{
pamDir: e.pamDir,
greetdPath: e.greetdPath,
dankshellPath: e.dankshellPath,
dankshellU2fPath: e.dankshellU2fPath,
isNixOS: func() bool { return isNixOS },
readFile: os.ReadFile,
stat: os.Stat,
createTemp: func(_ string, pattern string) (*os.File, error) {
return os.CreateTemp(e.tmpDir, pattern)
},
removeFile: os.Remove,
runSudoCmd: func(_ string, command string, args ...string) error {
switch command {
case "cp":
if len(args) != 2 {
return fmt.Errorf("unexpected cp args: %v", args)
}
data, err := os.ReadFile(args[0])
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(args[1]), 0o755); err != nil {
return err
}
return os.WriteFile(args[1], data, 0o644)
case "chmod":
if len(args) != 2 {
return fmt.Errorf("unexpected chmod args: %v", args)
}
return nil
case "rm":
if len(args) != 2 || args[0] != "-f" {
return fmt.Errorf("unexpected rm args: %v", args)
}
if err := os.Remove(args[1]); err != nil && !os.IsNotExist(err) {
return err
}
return nil
default:
return fmt.Errorf("unexpected sudo command: %s %v", command, args)
}
},
pamModuleExists: func(module string) bool {
return e.availableModules[module]
},
fingerprintAvailableForCurrentUser: func() bool {
return e.fingerprintAvailable
},
}
}
func readFileString(t *testing.T, path string) string { func readFileString(t *testing.T, path string) string {
t.Helper() t.Helper()
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
@@ -703,98 +614,6 @@ func TestSyncLockscreenU2FPamConfigWithDeps(t *testing.T) {
}) })
} }
func TestSyncGreeterPamConfigWithDeps(t *testing.T) {
t.Parallel()
t.Run("adds managed block for enabled auth modules", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_fprintd.so"] = true
env.availableModules["pam_u2f.so"] = true
env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so\naccount required pam_unix.so\n")
settings := AuthSettings{GreeterEnableFprint: true, GreeterEnableU2f: true}
if err := syncGreeterPamConfigWithDeps(func(string) {}, "", settings, false, env.deps(false)); err != nil {
t.Fatalf("syncGreeterPamConfigWithDeps returned error: %v", err)
}
got := readFileString(t, env.greetdPath)
for _, want := range []string{
GreeterPamManagedBlockStart,
"auth sufficient pam_fprintd.so max-tries=2 timeout=10",
"auth sufficient pam_u2f.so cue nouserok timeout=10",
GreeterPamManagedBlockEnd,
} {
if !strings.Contains(got, want) {
t.Errorf("missing expected string %q in greetd PAM:\n%s", want, got)
}
}
if strings.Index(got, GreeterPamManagedBlockStart) > strings.Index(got, "auth include system-auth") {
t.Fatalf("managed block was not inserted before first auth line:\n%s", got)
}
})
t.Run("avoids duplicate fingerprint when included stack already provides it", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_fprintd.so"] = true
env.fingerprintAvailable = true
original := "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n"
env.writePamFile(t, "greetd", original)
env.writePamFile(t, "system-auth", "auth sufficient pam_fprintd.so max-tries=1\nauth sufficient pam_unix.so\n")
settings := AuthSettings{GreeterEnableFprint: true}
if err := syncGreeterPamConfigWithDeps(func(string) {}, "", settings, false, env.deps(false)); err != nil {
t.Fatalf("syncGreeterPamConfigWithDeps returned error: %v", err)
}
got := readFileString(t, env.greetdPath)
if got != original {
t.Fatalf("greetd PAM changed despite included pam_fprintd stack\ngot:\n%s\nwant:\n%s", got, original)
}
if strings.Contains(got, GreeterPamManagedBlockStart) {
t.Fatalf("managed block should not be inserted when included stack already has pam_fprintd:\n%s", got)
}
})
}
func TestRemoveManagedGreeterPamBlockWithDeps(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.writePamFile(t, "greetd", "#%PAM-1.0\n"+
legacyGreeterPamFprintComment+"\n"+
"auth sufficient pam_fprintd.so max-tries=1\n"+
GreeterPamManagedBlockStart+"\n"+
"auth sufficient pam_u2f.so cue nouserok timeout=10\n"+
GreeterPamManagedBlockEnd+"\n"+
"auth include system-auth\n")
if err := removeManagedGreeterPamBlockWithDeps(func(string) {}, "", env.deps(false)); err != nil {
t.Fatalf("removeManagedGreeterPamBlockWithDeps returned error: %v", err)
}
got := readFileString(t, env.greetdPath)
if strings.Contains(got, GreeterPamManagedBlockStart) || strings.Contains(got, legacyGreeterPamFprintComment) {
t.Fatalf("managed or legacy DMS auth lines remained in greetd PAM:\n%s", got)
}
if !strings.Contains(got, "auth include system-auth") {
t.Fatalf("expected non-DMS greetd auth lines to remain:\n%s", got)
}
}
func (e *pamTestEnv) validateDeps() lockscreenPamValidateDeps {
return lockscreenPamValidateDeps{
baseDirs: []string{e.pamDir},
readFile: os.ReadFile,
stat: os.Stat,
pamModuleExists: func(module string) bool { return e.availableModules[module] },
}
}
func TestListLockscreenPamServices(t *testing.T) { func TestListLockscreenPamServices(t *testing.T) {
t.Parallel() t.Parallel()
@@ -1081,10 +900,98 @@ func containsSubstr(items []string, substr string) bool {
return false return false
} }
type pamTestEnv struct {
pamDir string
dankshellPath string
dankshellU2fPath string
tmpDir string
homeDir string
availableModules map[string]bool
}
func newPamTestEnv(t *testing.T) *pamTestEnv {
t.Helper()
root := t.TempDir()
pamDir := filepath.Join(root, "pam.d")
tmpDir := filepath.Join(root, "tmp")
homeDir := filepath.Join(root, "home")
for _, dir := range []string{pamDir, tmpDir, homeDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("failed to create %s: %v", dir, err)
}
}
return &pamTestEnv{
pamDir: pamDir,
dankshellPath: filepath.Join(pamDir, "dankshell"),
dankshellU2fPath: filepath.Join(pamDir, "dankshell-u2f"),
tmpDir: tmpDir,
homeDir: homeDir,
availableModules: map[string]bool{},
}
}
func (e *pamTestEnv) deps(isNixOS bool) syncDeps {
return syncDeps{
pamDir: e.pamDir,
dankshellPath: e.dankshellPath,
dankshellU2fPath: e.dankshellU2fPath,
isNixOS: func() bool { return isNixOS },
readFile: os.ReadFile,
stat: os.Stat,
createTemp: func(_ string, pattern string) (*os.File, error) {
return os.CreateTemp(e.tmpDir, pattern)
},
removeFile: os.Remove,
runSudoCmd: func(_ string, command string, args ...string) error {
switch command {
case "cp":
if len(args) != 2 {
return fmt.Errorf("unexpected cp args: %v", args)
}
data, err := os.ReadFile(args[0])
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(args[1]), 0o755); err != nil {
return err
}
return os.WriteFile(args[1], data, 0o644)
case "chmod":
if len(args) != 2 {
return fmt.Errorf("unexpected chmod args: %v", args)
}
return nil
case "rm":
if len(args) != 2 || args[0] != "-f" {
return fmt.Errorf("unexpected rm args: %v", args)
}
if err := os.Remove(args[1]); err != nil && !os.IsNotExist(err) {
return err
}
return nil
default:
return fmt.Errorf("unexpected sudo command: %s %v", command, args)
}
},
}
}
func (e *pamTestEnv) validateDeps() lockscreenPamValidateDeps {
return lockscreenPamValidateDeps{
baseDirs: []string{e.pamDir},
readFile: os.ReadFile,
stat: os.Stat,
pamModuleExists: func(module string) bool { return e.availableModules[module] },
}
}
func TestSyncAuthConfigWithDeps(t *testing.T) { func TestSyncAuthConfigWithDeps(t *testing.T) {
t.Parallel() t.Parallel()
t.Run("creates lockscreen targets and skips greetd when greeter is not installed", func(t *testing.T) { t.Run("creates lockscreen targets", func(t *testing.T) {
t.Parallel() t.Parallel()
env := newPamTestEnv(t) env := newPamTestEnv(t)
@@ -1092,10 +999,7 @@ func TestSyncAuthConfigWithDeps(t *testing.T) {
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n") env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\naccount required pam_access.so\n") env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\naccount required pam_access.so\n")
var logs []string err := syncAuthConfigWithDeps(func(string) {}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
err := syncAuthConfigWithDeps(func(msg string) {
logs = append(logs, msg)
}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
if err != nil { if err != nil {
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err) t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
} }
@@ -1106,105 +1010,24 @@ func TestSyncAuthConfigWithDeps(t *testing.T) {
if got := readFileString(t, env.dankshellU2fPath); got != buildManagedLockscreenU2FPamContent() { if got := readFileString(t, env.dankshellU2fPath); got != buildManagedLockscreenU2FPamContent() {
t.Fatalf("unexpected dankshell-u2f content:\n%s", got) t.Fatalf("unexpected dankshell-u2f content:\n%s", got)
} }
if len(logs) == 0 || !strings.Contains(logs[len(logs)-1], "greetd not found") {
t.Fatalf("expected greetd skip log, got %v", logs)
}
}) })
t.Run("separate greeter and lockscreen toggles are respected", func(t *testing.T) { t.Run("removes dankshell-u2f when disabled", func(t *testing.T) {
t.Parallel() t.Parallel()
env := newPamTestEnv(t) env := newPamTestEnv(t)
env.availableModules["pam_fprintd.so"] = true env.writeSettings(t, `{"enableU2f":false}`)
env.writeSettings(t, `{"enableU2f":false,"greeterEnableFprint":true,"greeterEnableU2f":false}`)
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n") env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\naccount required pam_access.so\n") env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\naccount required pam_access.so\n")
env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n") env.writePamFile(t, "dankshell-u2f", buildManagedLockscreenU2FPamContent())
err := syncAuthConfigWithDeps(func(string) {}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false)) err := syncAuthConfigWithDeps(func(string) {}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
if err != nil { if err != nil {
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err) t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
} }
dankshell := readFileString(t, env.dankshellPath)
if strings.Contains(dankshell, "pam_fprintd") || strings.Contains(dankshell, "pam_u2f") {
t.Fatalf("lockscreen PAM should strip fingerprint and U2F modules:\n%s", dankshell)
}
if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) { if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) {
t.Fatalf("expected dankshell-u2f to remain absent when enableU2f is false, stat err = %v", err) t.Fatalf("expected dankshell-u2f to be removed, stat err = %v", err)
}
greetd := readFileString(t, env.greetdPath)
if !strings.Contains(greetd, "auth sufficient pam_fprintd.so max-tries=2 timeout=10") {
t.Fatalf("expected greetd PAM to receive fingerprint auth block:\n%s", greetd)
}
if strings.Contains(greetd, "auth sufficient pam_u2f.so cue nouserok timeout=10") {
t.Fatalf("did not expect greetd PAM to receive U2F auth block:\n%s", greetd)
}
})
t.Run("externally managed greetd is stripped and greeter sync skipped", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_fprintd.so"] = true
env.writeSettings(t, `{"greeterPamExternallyManaged":true,"greeterEnableFprint":true}`)
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so\naccount required pam_unix.so\n")
env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\n"+
GreeterPamManagedBlockStart+"\n"+
"auth sufficient pam_fprintd.so max-tries=2 timeout=10\n"+
GreeterPamManagedBlockEnd+"\n")
var logs []string
err := syncAuthConfigWithDeps(func(msg string) {
logs = append(logs, msg)
}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
if err != nil {
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
}
greetd := readFileString(t, env.greetdPath)
if strings.Contains(greetd, GreeterPamManagedBlockStart) || strings.Contains(greetd, "pam_fprintd") {
t.Fatalf("expected DMS-managed block stripped from externally managed greetd:\n%s", greetd)
}
if !strings.Contains(greetd, "auth include system-auth") {
t.Fatalf("expected non-DMS greetd lines to remain:\n%s", greetd)
}
if !containsSubstr(logs, "externally managed") {
t.Fatalf("expected externally-managed skip log, got %v", logs)
}
})
t.Run("NixOS remains informational and non-mutating", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_fprintd.so"] = true
env.availableModules["pam_u2f.so"] = true
env.writeSettings(t, `{"enableU2f":true,"greeterEnableFprint":true,"greeterEnableU2f":true}`)
originalGreetd := "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n"
env.writePamFile(t, "greetd", originalGreetd)
var logs []string
err := syncAuthConfigWithDeps(func(msg string) {
logs = append(logs, msg)
}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(true))
if err != nil {
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
}
if _, err := os.Stat(env.dankshellPath); !os.IsNotExist(err) {
t.Fatalf("expected dankshell to remain absent on NixOS path, stat err = %v", err)
}
if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) {
t.Fatalf("expected dankshell-u2f to remain absent on NixOS path, stat err = %v", err)
}
if got := readFileString(t, env.greetdPath); got != originalGreetd {
t.Fatalf("expected greetd PAM to remain unchanged on NixOS path\ngot:\n%s\nwant:\n%s", got, originalGreetd)
}
if len(logs) < 2 || !strings.Contains(strings.Join(logs, "\n"), "NixOS detected") {
t.Fatalf("expected informational NixOS logs, got %v", logs)
} }
}) })
} }
@@ -1,25 +0,0 @@
package qmlchecks
import (
"os"
"strings"
"testing"
)
func TestGreeterExternalAuthStatusUsesEffectiveFingerprintAvailability(t *testing.T) {
data, err := os.ReadFile("../../../quickshell/Modules/Greetd/GreeterContent.qml")
if err != nil {
t.Fatalf("read greeter QML: %v", err)
}
content := string(data)
for _, required := range []string{
"readonly property bool greeterPamHasExternalAuth: greeterPamHasFprint || greeterPamHasU2f",
"if (greeterPamHasFprint && greeterPamHasU2f)",
"if (greeterPamHasFprint)",
} {
if !strings.Contains(content, required) {
t.Fatalf("greeter external-auth status must contain %q", required)
}
}
}
+2 -9
View File
@@ -7,7 +7,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter" "github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
@@ -260,13 +260,6 @@ func (m Model) installPackages() tea.Cmd {
// Run optional greeter setup // Run optional greeter setup
if msg.Phase == distros.PhaseComplete && msg.IsComplete && msg.Error == nil { if msg.Phase == distros.PhaseComplete && msg.IsComplete && msg.Error == nil {
if m.optionalDepSelected("dms-greeter") { if m.optionalDepSelected("dms-greeter") {
compositorName := "niri"
switch m.selectedWindowManager() {
case deps.WindowManagerHyprland:
compositorName = "Hyprland"
case deps.WindowManagerMango:
compositorName = "mango"
}
m.packageProgressChan <- packageInstallProgressMsg{ m.packageProgressChan <- packageInstallProgressMsg{
progress: 0.92, progress: 0.92,
step: "Configuring DMS greeter...", step: "Configuring DMS greeter...",
@@ -279,7 +272,7 @@ func (m Model) installPackages() tea.Cmd {
logOutput: line, logOutput: line,
} }
} }
if err := greeter.AutoSetupGreeter(compositorName, m.sudoPassword, greeterLogFunc); err != nil { if err := utils.RunDmsGreeterInstall(m.sudoPassword, greeterLogFunc); err != nil {
m.packageProgressChan <- packageInstallProgressMsg{ m.packageProgressChan <- packageInstallProgressMsg{
progress: 0.96, progress: 0.96,
step: "Greeter setup warning", step: "Greeter setup warning",
+40
View File
@@ -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()
}
-9
View File
@@ -1,9 +0,0 @@
<services>
<!-- Download dms-qml source tarball from GitHub releases (greeter + quickshell content) -->
<service name="download_url">
<param name="protocol">https</param>
<param name="host">github.com</param>
<param name="path">/AvengeMedia/DankMaterialShell/releases/download/v1.4.3/dms-qml.tar.gz</param>
<param name="filename">dms-qml.tar.gz</param>
</service>
</services>
@@ -1,5 +0,0 @@
dms-greeter (1.4.3db1) unstable; urgency=medium
* Update to v1.4.3 stable release
-- Avenge Media <AvengeMedia.US@gmail.com> Tue, 25 Feb 2026 02:40:00 +0000
-23
View File
@@ -1,23 +0,0 @@
Source: dms-greeter
Section: x11
Priority: optional
Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
Build-Depends: debhelper-compat (= 13)
Standards-Version: 4.6.2
Homepage: https://github.com/AvengeMedia/DankMaterialShell
Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell
Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
Package: dms-greeter
Architecture: any
Depends: ${misc:Depends},
greetd,
quickshell-git | quickshell
Suggests: niri | hyprland | sway
Description: DankMaterialShell greeter for greetd
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
inspired greeter interface built with Quickshell for Wayland compositors.
.
Supports multiple compositors including Niri, Hyprland, and Sway with automatic
compositor detection and configuration. Features session selection, user
authentication, and dynamic theming.
@@ -1,27 +0,0 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: dms-greeter
Upstream-Contact: Avenge Media LLC <AvengeMedia.US@gmail.com>
Source: https://github.com/AvengeMedia/DankMaterialShell
Files: *
Copyright: 2026 Avenge Media LLC
License: MIT
License: MIT
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-108
View File
@@ -1,108 +0,0 @@
#!/bin/sh
set -e
case "$1" in
configure)
# Create greeter user/group if they don't exist
if ! getent group greeter >/dev/null; then
addgroup --system greeter
fi
if ! getent passwd greeter >/dev/null; then
adduser --system --ingroup greeter --home /var/lib/greeter \
--shell /bin/bash --gecos "System Greeter" greeter
fi
if [ -d /var/cache/dms-greeter ]; then
chown -R greeter:greeter /var/cache/dms-greeter 2>/dev/null || true
fi
if [ -d /var/lib/greeter ]; then
chown -R greeter:greeter /var/lib/greeter 2>/dev/null || true
fi
# Check and set graphical.target as default
CURRENT_TARGET=$(systemctl get-default 2>/dev/null || echo "unknown")
if [ "$CURRENT_TARGET" != "graphical.target" ]; then
systemctl set-default graphical.target >/dev/null 2>&1 || true
TARGET_STATUS="Set to graphical.target (was: $CURRENT_TARGET) ✓"
else
TARGET_STATUS="Already graphical.target ✓"
fi
GREETD_CONFIG="/etc/greetd/config.toml"
CONFIG_STATUS="Not modified (already configured)"
# Check if niri or hyprland exists
COMPOSITOR="niri"
if ! command -v niri >/dev/null 2>&1; then
if command -v Hyprland >/dev/null 2>&1; then
COMPOSITOR="hyprland"
fi
fi
# If config doesn't exist, create a default one
if [ ! -f "$GREETD_CONFIG" ]; then
mkdir -p /etc/greetd
cat > "$GREETD_CONFIG" << 'GREETD_EOF'
[terminal]
vt = 1
[default_session]
user = "greeter"
command = "/usr/bin/dms-greeter --command COMPOSITOR_PLACEHOLDER"
GREETD_EOF
sed -i "s|COMPOSITOR_PLACEHOLDER|$COMPOSITOR|" "$GREETD_CONFIG"
CONFIG_STATUS="Created new config with $COMPOSITOR ✓"
elif ! grep -q "dms-greeter" "$GREETD_CONFIG"; then
# Backup existing config
BACKUP_FILE="${GREETD_CONFIG}.backup-$(date +%Y%m%d-%H%M%S)"
cp "$GREETD_CONFIG" "$BACKUP_FILE" 2>/dev/null || true
# Update command in default_session section
sed -i "/^\[default_session\]/,/^\[/ s|^command =.*|command = \"/usr/bin/dms-greeter --command $COMPOSITOR\"|" "$GREETD_CONFIG"
sed -i '/^\[default_session\]/,/^\[/ s|^user =.*|user = "greeter"|' "$GREETD_CONFIG"
CONFIG_STATUS="Updated existing config (backed up) with $COMPOSITOR ✓"
fi
# Only show banner on initial install
if [ -z "$2" ]; then
cat << 'EOF'
=========================================================================
DMS Greeter Installation Complete!
=========================================================================
Status:
EOF
echo " ✓ Greetd config: $CONFIG_STATUS"
echo " ✓ Default target: $TARGET_STATUS"
cat << 'EOF'
✓ Greeter user: Created
✓ Greeter directories: /var/cache/dms-greeter, /var/lib/greeter
Next steps:
1. Enable the greeter:
dms greeter enable
(This will automatically disable conflicting display managers,
set graphical.target, and enable greetd)
2. Sync your theme with the greeter (optional):
dms greeter sync
3. Check your setup:
dms greeter status
Ready to test? Run: sudo systemctl start greetd
Documentation: https://danklinux.com/docs/dankgreeter/
=========================================================================
EOF
fi
;;
esac
#DEBHELPER#
exit 0
-14
View File
@@ -1,14 +0,0 @@
#!/bin/sh
set -e
case "$1" in
purge)
# Remove greeter cache directory on purge
rm -rf /var/cache/dms-greeter 2>/dev/null || true
;;
esac
#DEBHELPER#
exit 0
-50
View File
@@ -1,50 +0,0 @@
#!/usr/bin/make -f
export DH_VERBOSE = 1
DEB_VERSION := $(shell dpkg-parsechangelog -S Version)
UPSTREAM_VERSION := $(shell echo $(DEB_VERSION) | sed 's/-[^-]*$$//')
%:
dh $@
override_dh_auto_build:
: nothing to build, we use prebuilt tarball content
override_dh_auto_install:
# Same pattern as dms: upstream from combined tarball (native format)
# Build root is either . (we're inside dms-qml) or has dms-qml/ subdir
SOURCE_DIR=""; \
if [ -d dms-qml ]; then SOURCE_DIR="dms-qml"; \
elif [ -f Modules/Greetd/assets/dms-greeter ]; then SOURCE_DIR="."; \
fi; \
if [ -n "$$SOURCE_DIR" ]; then \
mkdir -p debian/dms-greeter/usr/share/quickshell/dms-greeter && \
( cd $$SOURCE_DIR && tar chf - --exclude=debian . ) | \
( cd debian/dms-greeter/usr/share/quickshell/dms-greeter && tar xf - ) && \
install -Dm755 $$SOURCE_DIR/Modules/Greetd/assets/dms-greeter \
debian/dms-greeter/usr/bin/dms-greeter && \
install -Dm644 $$SOURCE_DIR/Modules/Greetd/README.md \
debian/dms-greeter/usr/share/doc/dms-greeter/README.md && \
install -Dm644 $$SOURCE_DIR/LICENSE \
debian/dms-greeter/usr/share/doc/dms-greeter/LICENSE && \
install -Dpm0644 $$SOURCE_DIR/systemd/tmpfiles-dms-greeter.conf \
debian/dms-greeter/usr/lib/tmpfiles.d/dms-greeter.conf && \
install -Dm644 $$SOURCE_DIR/systemd/sysusers-dms-greeter.conf \
debian/dms-greeter/usr/lib/sysusers.d/dms-greeter.conf; \
else \
echo "ERROR: No upstream source (dms-qml or Modules/Greetd/assets/dms-greeter)!" && \
echo "Contents of current directory:" && ls -la && exit 1; \
fi
# Remove build and development files
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/core
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/distro
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/.git*
rm -f debian/dms-greeter/usr/share/quickshell/dms-greeter/.gitignore
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/.github
override_dh_auto_clean:
rm -rf dms-qml
# When build root is dms-qml itself, we're inside it - nothing extra to remove
dh_auto_clean
@@ -1 +0,0 @@
3.0 (native)
@@ -1 +0,0 @@
# OBS _service downloads dms-qml.tar.gz; no extra excludes needed
-263
View File
@@ -1,263 +0,0 @@
# Spec for DMS Greeter - Stable releases
%global debug_package %{nil}
%global version VERSION_PLACEHOLDER
%global pkg_summary DankMaterialShell greeter for greetd
Name: dms-greeter
Version: %{version}
Release: RELEASE_PLACEHOLDER%{?dist}
Summary: %{pkg_summary}
License: MIT
URL: https://github.com/AvengeMedia/DankMaterialShell
Source0: dms-qml.tar.gz
BuildRequires: gzip
BuildRequires: wget
BuildRequires: systemd-rpm-macros
Requires: greetd
Requires: (quickshell-git or quickshell)
Requires(post): /usr/sbin/useradd
Requires(post): /usr/sbin/groupadd
Recommends: policycoreutils-python-utils
Recommends: acl
Suggests: niri
Suggests: hyprland
Suggests: sway
%description
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
inspired greeter interface built with Quickshell for Wayland compositors.
Supports multiple compositors including Niri, Hyprland, and Sway with automatic
compositor detection and configuration. Features session selection, user
authentication, and dynamic theming.
%prep
%setup -q -c -n dms-qml
%build
%install
# Install greeter files to shared data location
install -dm755 %{buildroot}%{_datadir}/quickshell/dms-greeter
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms-greeter/
install -Dm755 %{_builddir}/dms-qml/Modules/Greetd/assets/dms-greeter %{buildroot}%{_bindir}/dms-greeter
install -Dm644 %{_builddir}/dms-qml/Modules/Greetd/README.md %{buildroot}%{_docdir}/dms-greeter/README.md
install -Dpm0644 %{_builddir}/dms-qml/systemd/tmpfiles-dms-greeter.conf %{buildroot}%{_tmpfilesdir}/dms-greeter.conf
install -Dm644 %{_builddir}/dms-qml/systemd/sysusers-dms-greeter.conf %{buildroot}%{_sysusersdir}/dms-greeter.conf
install -Dm644 %{_builddir}/dms-qml/LICENSE %{buildroot}%{_docdir}/dms-greeter/LICENSE
install -dm755 %{buildroot}%{_sharedstatedir}/greeter
# Note: We do NOT install a PAM config here to avoid conflicting w/greetd packages
# Remove build and development files
rm -rf %{buildroot}%{_datadir}/quickshell/dms-greeter/.git*
rm -f %{buildroot}%{_datadir}/quickshell/dms-greeter/.gitignore
rm -rf %{buildroot}%{_datadir}/quickshell/dms-greeter/.github
rm -rf %{buildroot}%{_datadir}/quickshell/dms-greeter/distro
%posttrans
if [ -d "%{_sysconfdir}/xdg/quickshell/dms-greeter" ]; then
# Remove directories & preserves any user-added files
rmdir "%{_sysconfdir}/xdg/quickshell/dms-greeter" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
fi
%files
%license %{_docdir}/dms-greeter/LICENSE
%doc %{_docdir}/dms-greeter/README.md
%{_bindir}/dms-greeter
%{_datadir}/quickshell/dms-greeter/
%{_tmpfilesdir}/%{name}.conf
%{_sysusersdir}/dms-greeter.conf
%pre
# Create greeter user/group if they don't exist
getent group greeter >/dev/null || groupadd -r greeter
getent passwd greeter >/dev/null || \
useradd -r -g greeter -d %{_sharedstatedir}/greeter -s /bin/bash \
-c "System Greeter" greeter
exit 0
%post
# Set SELinux contexts for greeter files on Fedora systems
if [ -x /usr/sbin/semanage ] && [ -x /usr/sbin/restorecon ]; then
# Greeter launcher binary
semanage fcontext -a -t bin_t '%{_bindir}/dms-greeter' >/dev/null 2>&1 || true
restorecon %{_bindir}/dms-greeter >/dev/null 2>&1 || true
# Greeter home directory
semanage fcontext -a -t user_home_dir_t '%{_sharedstatedir}/greeter(/.*)?' >/dev/null 2>&1 || true
restorecon -R %{_sharedstatedir}/greeter >/dev/null 2>&1 || true
# Cache directory for greeter data
semanage fcontext -a -t cache_home_t '%{_localstatedir}/cache/dms-greeter(/.*)?' >/dev/null 2>&1 || true
restorecon -R %{_localstatedir}/cache/dms-greeter >/dev/null 2>&1 || true
# Shared data directory
semanage fcontext -a -t usr_t '%{_datadir}/quickshell/dms-greeter(/.*)?' >/dev/null 2>&1 || true
restorecon -R %{_datadir}/quickshell/dms-greeter >/dev/null 2>&1 || true
# PAM configuration
restorecon %{_sysconfdir}/pam.d/greetd >/dev/null 2>&1 || true
fi
# Ensure proper ownership of greeter directories
chown -R greeter:greeter %{_localstatedir}/cache/dms-greeter 2>/dev/null || true
chown -R greeter:greeter %{_sharedstatedir}/greeter 2>/dev/null || true
# Verify PAM configuration - only fix if insufficient
PAM_CONFIG="/etc/pam.d/greetd"
if [ ! -f "$PAM_CONFIG" ]; then
cat > "$PAM_CONFIG" << 'PAM_EOF'
#%PAM-1.0
auth substack system-auth
auth include postlogin
account required pam_nologin.so
account include system-auth
password include system-auth
session required pam_selinux.so close
session required pam_loginuid.so
session required pam_selinux.so open
session optional pam_keyinit.so force revoke
session include system-auth
session include postlogin
PAM_EOF
chmod 644 "$PAM_CONFIG"
# Only show message on initial install
[ "$1" -eq 1 ] && echo "Created PAM configuration for greetd"
elif ! grep -q "pam_systemd\|system-auth" "$PAM_CONFIG"; then
cp "$PAM_CONFIG" "$PAM_CONFIG.backup-dms-greeter"
cat > "$PAM_CONFIG" << 'PAM_EOF'
#%PAM-1.0
auth substack system-auth
auth include postlogin
account required pam_nologin.so
account include system-auth
password include system-auth
session required pam_selinux.so close
session required pam_loginuid.so
session required pam_selinux.so open
session optional pam_keyinit.so force revoke
session include system-auth
session include postlogin
PAM_EOF
chmod 644 "$PAM_CONFIG"
# Only show message on initial install
[ "$1" -eq 1 ] && echo "Updated PAM configuration (old config backed up to $PAM_CONFIG.backup-dms-greeter)"
fi
# Auto-configure greetd config
GREETD_CONFIG="/etc/greetd/config.toml"
CONFIG_STATUS="Not modified (already configured)"
# Check if niri or hyprland exists
COMPOSITOR="niri"
if ! command -v niri >/dev/null 2>&1; then
if command -v Hyprland >/dev/null 2>&1; then
COMPOSITOR="hyprland"
fi
fi
# If config doesn't exist, create a default one
if [ ! -f "$GREETD_CONFIG" ]; then
mkdir -p /etc/greetd
cat > "$GREETD_CONFIG" << 'GREETD_EOF'
[terminal]
vt = 1
[default_session]
user = "greeter"
command = "/usr/bin/dms-greeter --command COMPOSITOR_PLACEHOLDER"
GREETD_EOF
sed -i "s|COMPOSITOR_PLACEHOLDER|$COMPOSITOR|" "$GREETD_CONFIG"
CONFIG_STATUS="Created new config with $COMPOSITOR "
elif ! grep -q "dms-greeter" "$GREETD_CONFIG"; then
BACKUP_FILE="${GREETD_CONFIG}.backup-$(date +%%Y%%m%%d-%%H%%M%%S)"
cp "$GREETD_CONFIG" "$BACKUP_FILE" 2>/dev/null || true
# Update command in default_session section
sed -i "/^\[default_session\]/,/^\[/ s|^command =.*|command = \"/usr/bin/dms-greeter --command $COMPOSITOR\"|" "$GREETD_CONFIG"
sed -i '/^\[default_session\]/,/^\[/ s|^user =.*|user = "greeter"|' "$GREETD_CONFIG"
CONFIG_STATUS="Updated existing config (backed up) with $COMPOSITOR "
fi
# Set graphical.target as default
CURRENT_TARGET=$(systemctl get-default 2>/dev/null || echo "unknown")
if [ "$CURRENT_TARGET" != "graphical.target" ]; then
systemctl set-default graphical.target >/dev/null 2>&1 || true
TARGET_STATUS="Set to graphical.target (was: $CURRENT_TARGET) "
else
TARGET_STATUS="Already graphical.target "
fi
# Only show banner on initial install
if [ "$1" -eq 1 ]; then
cat << 'EOF'
=========================================================================
DMS Greeter Installation Complete!
=========================================================================
Status:
EOF
echo " Greetd config: $CONFIG_STATUS"
echo " Default target: $TARGET_STATUS"
cat << 'EOF'
Greeter user: Created
Greeter directories: /var/cache/dms-greeter, /var/lib/greeter
SELinux contexts: Applied
Next steps:
1. Enable the greeter:
dms greeter enable
(This will automatically disable gdm, sddm, or lightdm display managers,
set graphical.target, and enable greetd)
2. Sync your theme with the greeter (optional):
If you have DankMaterialShell (DMS) installed, you can sync theming with:
dms greeter sync
3. Check your setup:
dms greeter status
Ready to test? Run: sudo systemctl start greetd or simply log out/reboot your system
Documentation: https://danklinux.com/docs/dankgreeter/
=========================================================================
EOF
fi
%postun
# Clean up SELinux contexts on package removal
if [ "$1" -eq 0 ] && [ -x /usr/sbin/semanage ]; then
semanage fcontext -d '%{_bindir}/dms-greeter' 2>/dev/null || true
semanage fcontext -d '%{_sharedstatedir}/greeter(/.*)?' 2>/dev/null || true
semanage fcontext -d '%{_localstatedir}/cache/dms-greeter(/.*)?' 2>/dev/null || true
semanage fcontext -d '%{_datadir}/quickshell/dms-greeter(/.*)?' 2>/dev/null || true
fi
%changelog
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER
- Stable release VERSION_PLACEHOLDER
- Built from GitHub release
-290
View File
@@ -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"
];
};
}
-8
View File
@@ -9,7 +9,6 @@ rec {
paths = [ paths = [
nixos-module nixos-module
nixos-service-start-module nixos-service-start-module
greeter-niri-module
niri-home-module niri-home-module
home-manager-module home-manager-module
]; ];
@@ -29,13 +28,6 @@ rec {
; ;
}; };
greeter-niri-module = import ./greeter-niri-module.nix {
inherit
self
pkgs
;
};
niri-home-module = import ./niri-home-module.nix { niri-home-module = import ./niri-home-module.nix {
inherit inherit
self self
-71
View File
@@ -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)
'';
}
-325
View File
@@ -1,325 +0,0 @@
# Spec for DMS Greeter - OpenSUSE/OBS
%global debug_package %{nil}
%global version VERSION_PLACEHOLDER
%global pkg_summary DankMaterialShell greeter for greetd
Name: dms-greeter
Version: %{version}
Release: RELEASE_PLACEHOLDER%{?dist}
Summary: %{pkg_summary}
License: MIT
URL: https://github.com/AvengeMedia/DankMaterialShell
Source0: dms-qml.tar.gz
BuildRequires: gzip
BuildRequires: wget
BuildRequires: systemd-rpm-macros
Requires: greetd
Requires: (quickshell-git or quickshell)
Requires(post): /usr/sbin/useradd
Requires(post): /usr/sbin/groupadd
Recommends: policycoreutils-python-utils
Recommends: acl
Suggests: niri
Suggests: hyprland
Suggests: sway
%description
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
inspired greeter interface built with Quickshell for Wayland compositors.
Supports multiple compositors including Niri, Hyprland, and Sway with automatic
compositor detection and configuration. Features session selection, user
authentication, and dynamic theming.
%prep
%setup -q -c -n dms-qml
%build
%install
# Install greeter files to shared data location
install -dm755 %{buildroot}%{_datadir}/quickshell/dms-greeter
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms-greeter/
install -Dm755 %{_builddir}/dms-qml/Modules/Greetd/assets/dms-greeter %{buildroot}%{_bindir}/dms-greeter
install -Dm644 %{_builddir}/dms-qml/Modules/Greetd/README.md %{buildroot}%{_docdir}/dms-greeter/README.md
install -Dpm0644 %{_builddir}/dms-qml/systemd/tmpfiles-dms-greeter.conf %{buildroot}%{_tmpfilesdir}/dms-greeter.conf
install -Dm644 %{_builddir}/dms-qml/systemd/sysusers-dms-greeter.conf %{buildroot}%{_sysusersdir}/dms-greeter.conf
install -Dm644 %{_builddir}/dms-qml/LICENSE %{buildroot}%{_docdir}/dms-greeter/LICENSE
install -dm755 %{buildroot}%{_sharedstatedir}/greeter
# Remove build and development files
rm -rf %{buildroot}%{_datadir}/quickshell/dms-greeter/.git*
rm -f %{buildroot}%{_datadir}/quickshell/dms-greeter/.gitignore
rm -rf %{buildroot}%{_datadir}/quickshell/dms-greeter/.github
rm -rf %{buildroot}%{_datadir}/quickshell/dms-greeter/distro
%posttrans
if [ -d "%{_sysconfdir}/xdg/quickshell/dms-greeter" ]; then
rmdir "%{_sysconfdir}/xdg/quickshell/dms-greeter" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
fi
%files
%dir %{_docdir}/dms-greeter
%license %{_docdir}/dms-greeter/LICENSE
%doc %{_docdir}/dms-greeter/README.md
%{_bindir}/dms-greeter
%dir %{_datadir}/quickshell
%{_datadir}/quickshell/dms-greeter/
%{_tmpfilesdir}/%{name}.conf
%{_sysusersdir}/dms-greeter.conf
%pre
# Create greeter user/group if they don't exist
getent group greeter >/dev/null || groupadd -r greeter
getent passwd greeter >/dev/null || \
useradd -r -g greeter -d %{_sharedstatedir}/greeter -s /bin/bash \
-c "System Greeter" greeter
exit 0
%post
# SELinux contexts (no-op on OpenSUSE - semanage/restorecon not present)
if [ -x /usr/sbin/semanage ] && [ -x /usr/sbin/restorecon ]; then
semanage fcontext -a -t bin_t '%{_bindir}/dms-greeter' >/dev/null 2>&1 || true
restorecon %{_bindir}/dms-greeter >/dev/null 2>&1 || true
semanage fcontext -a -t user_home_dir_t '%{_sharedstatedir}/greeter(/.*)?' >/dev/null 2>&1 || true
restorecon -R %{_sharedstatedir}/greeter >/dev/null 2>&1 || true
semanage fcontext -a -t cache_home_t '%{_localstatedir}/cache/dms-greeter(/.*)?' >/dev/null 2>&1 || true
restorecon -R %{_localstatedir}/cache/dms-greeter >/dev/null 2>&1 || true
semanage fcontext -a -t usr_t '%{_datadir}/quickshell/dms-greeter(/.*)?' >/dev/null 2>&1 || true
restorecon -R %{_datadir}/quickshell/dms-greeter >/dev/null 2>&1 || true
restorecon %{_sysconfdir}/pam.d/greetd >/dev/null 2>&1 || true
fi
# Resolve greeter runtime account/group for distro differences
GREETER_USER="greeter"
for candidate in greeter greetd _greeter; do
if getent passwd "$candidate" >/dev/null 2>&1; then
GREETER_USER="$candidate"
break
fi
done
GREETER_GROUP="$GREETER_USER"
if ! getent group "$GREETER_GROUP" >/dev/null 2>&1; then
for candidate in greeter greetd _greeter; do
if getent group "$candidate" >/dev/null 2>&1; then
GREETER_GROUP="$candidate"
break
fi
done
fi
# Ensure proper ownership of greeter directories
chown -R "$GREETER_USER:$GREETER_GROUP" %{_localstatedir}/cache/dms-greeter 2>/dev/null || true
chown -R "$GREETER_USER:$GREETER_GROUP" %{_sharedstatedir}/greeter 2>/dev/null || true
# Verify PAM configuration
PAM_CONFIG="/etc/pam.d/greetd"
write_greetd_pam_config() {
# openSUSE and Debian families usually expose PAM stacks as common-*
if [ -f /etc/pam.d/common-auth ] && [ -f /etc/pam.d/common-account ] && [ -f /etc/pam.d/common-password ] && [ -f /etc/pam.d/common-session ]; then
cat > "$PAM_CONFIG" << 'PAM_EOF'
#%PAM-1.0
auth include common-auth
account required pam_nologin.so
account include common-account
password include common-password
session required pam_loginuid.so
session optional pam_keyinit.so force revoke
session include common-session
PAM_EOF
return
fi
# Fedora/RHEL style system-auth/postlogin stack
if [ -f /etc/pam.d/system-auth ]; then
if [ -f /etc/pam.d/postlogin ]; then
cat > "$PAM_CONFIG" << 'PAM_EOF'
#%PAM-1.0
auth substack system-auth
auth include postlogin
account required pam_nologin.so
account include system-auth
password include system-auth
session required pam_loginuid.so
session optional pam_keyinit.so force revoke
session include system-auth
session include postlogin
PAM_EOF
else
cat > "$PAM_CONFIG" << 'PAM_EOF'
#%PAM-1.0
auth include system-auth
account required pam_nologin.so
account include system-auth
password include system-auth
session required pam_loginuid.so
session optional pam_keyinit.so force revoke
session include system-auth
PAM_EOF
fi
return
fi
# Last-resort conservative fallback
cat > "$PAM_CONFIG" << 'PAM_EOF'
#%PAM-1.0
auth required pam_unix.so nullok
account required pam_unix.so
password required pam_unix.so nullok sha512
session required pam_unix.so
PAM_EOF
}
if [ ! -f "$PAM_CONFIG" ]; then
write_greetd_pam_config
chmod 644 "$PAM_CONFIG"
[ "$1" -eq 1 ] && echo "Created PAM configuration for greetd"
else
NEEDS_PAM_UPDATE=0
if grep -q "common-auth" "$PAM_CONFIG"; then
if [ ! -f /etc/pam.d/common-auth ]; then
NEEDS_PAM_UPDATE=1
fi
elif grep -q "system-auth" "$PAM_CONFIG"; then
if [ ! -f /etc/pam.d/system-auth ]; then
NEEDS_PAM_UPDATE=1
fi
else
NEEDS_PAM_UPDATE=1
fi
if [ "$NEEDS_PAM_UPDATE" -eq 1 ]; then
cp "$PAM_CONFIG" "$PAM_CONFIG.backup-dms-greeter"
write_greetd_pam_config
chmod 644 "$PAM_CONFIG"
[ "$1" -eq 1 ] && echo "Updated PAM configuration (old config backed up to $PAM_CONFIG.backup-dms-greeter)"
fi
fi
# Auto-configure greetd config
GREETD_CONFIG="/etc/greetd/config.toml"
CONFIG_STATUS="Not modified (already configured)"
COMPOSITOR=""
for candidate in niri Hyprland sway; do
if command -v "$candidate" >/dev/null 2>&1; then
case "$candidate" in
Hyprland)
COMPOSITOR="hyprland"
;;
*)
COMPOSITOR="$candidate"
;;
esac
break
fi
done
if [ ! -f "$GREETD_CONFIG" ]; then
mkdir -p /etc/greetd
if [ -n "$COMPOSITOR" ]; then
cat > "$GREETD_CONFIG" << 'GREETD_EOF'
[terminal]
vt = 1
[default_session]
user = "GREETER_USER_PLACEHOLDER"
command = "/usr/bin/dms-greeter --command COMPOSITOR_PLACEHOLDER"
GREETD_EOF
sed -i "s|GREETER_USER_PLACEHOLDER|$GREETER_USER|" "$GREETD_CONFIG"
sed -i "s|COMPOSITOR_PLACEHOLDER|$COMPOSITOR|" "$GREETD_CONFIG"
CONFIG_STATUS="Created new config with $COMPOSITOR "
else
cat > "$GREETD_CONFIG" << 'GREETD_EOF'
[terminal]
vt = 1
[default_session]
user = "GREETER_USER_PLACEHOLDER"
command = "agreety --cmd /bin/login"
GREETD_EOF
sed -i "s|GREETER_USER_PLACEHOLDER|$GREETER_USER|" "$GREETD_CONFIG"
CONFIG_STATUS="Created safe fallback config (no supported compositor detected)"
fi
elif ! grep -q "dms-greeter" "$GREETD_CONFIG"; then
if [ -n "$COMPOSITOR" ]; then
BACKUP_FILE="${GREETD_CONFIG}.backup-$(date +%%Y%%m%%d-%%H%%M%%S)"
cp "$GREETD_CONFIG" "$BACKUP_FILE" 2>/dev/null || true
sed -i "/^\[default_session\]/,/^\[/ s|^command =.*|command = \"/usr/bin/dms-greeter --command $COMPOSITOR\"|" "$GREETD_CONFIG"
sed -i "/^\[default_session\]/,/^\[/ s|^user =.*|user = \"$GREETER_USER\"|" "$GREETD_CONFIG"
CONFIG_STATUS="Updated existing config (backed up) with $COMPOSITOR "
else
CONFIG_STATUS="Skipped dms-greeter command update (no supported compositor detected)"
fi
fi
# Set graphical.target as default
CURRENT_TARGET=$(systemctl get-default 2>/dev/null || echo "unknown")
if [ "$CURRENT_TARGET" != "graphical.target" ]; then
systemctl set-default graphical.target >/dev/null 2>&1 || true
TARGET_STATUS="Set to graphical.target (was: $CURRENT_TARGET) "
else
TARGET_STATUS="Already graphical.target "
fi
if [ "$1" -eq 1 ]; then
cat << 'EOF'
=========================================================================
DMS Greeter Installation Complete!
=========================================================================
Status:
EOF
echo " Greetd config: $CONFIG_STATUS"
echo " Default target: $TARGET_STATUS"
cat << 'EOF'
Greeter user: Created
Greeter directories: /var/cache/dms-greeter, /var/lib/greeter
SELinux contexts: Applied (if applicable)
Next steps:
1. Enable the greeter:
dms greeter enable
2. Sync your theme with the greeter (optional):
dms greeter sync
3. Check your setup:
dms greeter status
Ready to test? Run: sudo systemctl start greetd
Documentation: https://danklinux.com/docs/dankgreeter/
=========================================================================
EOF
fi
%postun
if [ "$1" -eq 0 ] && [ -x /usr/sbin/semanage ]; then
semanage fcontext -d '%{_bindir}/dms-greeter' 2>/dev/null || true
semanage fcontext -d '%{_sharedstatedir}/greeter(/.*)?' 2>/dev/null || true
semanage fcontext -d '%{_localstatedir}/cache/dms-greeter(/.*)?' 2>/dev/null || true
semanage fcontext -d '%{_datadir}/quickshell/dms-greeter(/.*)?' 2>/dev/null || true
fi
%changelog
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER
- Stable release VERSION_PLACEHOLDER
- Initial OpenSUSE/OBS port from Fedora
+1 -4
View File
@@ -5,7 +5,6 @@ set -euo pipefail
# Usage: ./copr-upload.sh [PACKAGE] [VERSION] [RELEASE] # Usage: ./copr-upload.sh [PACKAGE] [VERSION] [RELEASE]
# Examples: # Examples:
# ./copr-upload.sh dms 1.0.3 1 # ./copr-upload.sh dms 1.0.3 1
# ./copr-upload.sh dms-greeter 1.0.3 1
PACKAGE="${1:-dms}" PACKAGE="${1:-dms}"
VERSION="${2:-}" VERSION="${2:-}"
@@ -17,11 +16,9 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
# Determine Copr project based on package # Determine Copr project based on package
if [ "$PACKAGE" = "dms" ]; then if [ "$PACKAGE" = "dms" ]; then
COPR_PROJECT="avengemedia/dms" COPR_PROJECT="avengemedia/dms"
elif [ "$PACKAGE" = "dms-greeter" ]; then
COPR_PROJECT="avengemedia/danklinux"
else else
echo "❌ Unknown package: $PACKAGE" echo "❌ Unknown package: $PACKAGE"
echo "Supported packages: dms, dms-greeter" echo "Supported packages: dms"
exit 1 exit 1
fi fi
+1 -4
View File
@@ -11,7 +11,7 @@
OBS_BASE_PROJECT="home:AvengeMedia" OBS_BASE_PROJECT="home:AvengeMedia"
OBS_BASE="$HOME/.cache/osc-checkouts" OBS_BASE="$HOME/.cache/osc-checkouts"
ALL_PACKAGES=(dms dms-git dms-greeter) ALL_PACKAGES=(dms dms-git)
REPOS=("Debian_13" "openSUSE_Tumbleweed" "16.0") REPOS=("Debian_13" "openSUSE_Tumbleweed" "16.0")
ARCHES=("x86_64" "aarch64") ARCHES=("x86_64" "aarch64")
@@ -41,9 +41,6 @@ for pkg in "${PACKAGES[@]}"; do
dms-git) dms-git)
PROJECT="$OBS_BASE_PROJECT:dms-git" PROJECT="$OBS_BASE_PROJECT:dms-git"
;; ;;
dms-greeter)
PROJECT="$OBS_BASE_PROJECT:danklinux"
;;
*) *)
echo "Error: Unknown package '$pkg'" echo "Error: Unknown package '$pkg'"
continue continue
+2 -141
View File
@@ -68,14 +68,13 @@ fi
OBS_BASE_PROJECT="home:AvengeMedia" OBS_BASE_PROJECT="home:AvengeMedia"
OBS_BASE="$HOME/.cache/osc-checkouts" OBS_BASE="$HOME/.cache/osc-checkouts"
AVAILABLE_PACKAGES=(dms dms-git dms-greeter) AVAILABLE_PACKAGES=(dms dms-git)
if [[ -z "$PACKAGE" ]]; then if [[ -z "$PACKAGE" ]]; then
echo "Available packages:" echo "Available packages:"
echo "" echo ""
echo " 1. dms - Stable DMS" echo " 1. dms - Stable DMS"
echo " 2. dms-git - Nightly DMS" echo " 2. dms-git - Nightly DMS"
echo " 3. dms-greeter - DMS greeter for greetd"
echo " a. all" echo " a. all"
echo "" echo ""
read -r -p "Select package (1-${#AVAILABLE_PACKAGES[@]}, a): " selection read -r -p "Select package (1-${#AVAILABLE_PACKAGES[@]}, a): " selection
@@ -221,22 +220,6 @@ update_debian_dms_service() {
sed -i "s|/releases/download/v[0-9][^\"]*/dms-distropkg-arm64\.gz|/releases/download/v${base_version}/dms-distropkg-arm64.gz|" "$service_path" sed -i "s|/releases/download/v[0-9][^\"]*/dms-distropkg-arm64\.gz|/releases/download/v${base_version}/dms-distropkg-arm64.gz|" "$service_path"
} }
update_debian_dms_greeter_service() {
local service_path="$1"
if [[ -z "$service_path" || ! -f "$service_path" ]]; then
return 0
fi
if [[ -z "$CHANGELOG_VERSION" ]]; then
return 0
fi
local base_version
base_version=$(echo "$CHANGELOG_VERSION" | sed -E 's/^([0-9]+(\.[0-9]+)*).*/\1/')
if [[ -z "$base_version" ]]; then
return 0
fi
sed -i "s|/releases/download/v[0-9][^\"]*/dms-qml\.tar\.gz|/releases/download/v${base_version}/dms-qml.tar.gz|" "$service_path"
}
update_opensuse_git_spec() { update_opensuse_git_spec() {
local spec_path="$1" local spec_path="$1"
local go_ver local go_ver
@@ -323,9 +306,6 @@ dms)
dms-git) dms-git)
PROJECT="dms-git" PROJECT="dms-git"
;; ;;
dms-greeter)
PROJECT="danklinux"
;;
*) *)
echo "Error: Unknown package '$PACKAGE'" echo "Error: Unknown package '$PACKAGE'"
exit 1 exit 1
@@ -425,8 +405,6 @@ if [[ -d "distro/debian/$PACKAGE/debian" ]]; then
# Keep Debian _service in sync with changelog version # Keep Debian _service in sync with changelog version
if [[ "$PACKAGE" == "dms" ]] && [[ -f "distro/debian/$PACKAGE/_service" ]]; then if [[ "$PACKAGE" == "dms" ]] && [[ -f "distro/debian/$PACKAGE/_service" ]]; then
update_debian_dms_service "distro/debian/$PACKAGE/_service" update_debian_dms_service "distro/debian/$PACKAGE/_service"
elif [[ "$PACKAGE" == "dms-greeter" ]] && [[ -f "distro/debian/$PACKAGE/_service" ]]; then
update_debian_dms_greeter_service "distro/debian/$PACKAGE/_service"
fi fi
# Check if this version already exists in OBS # Check if this version already exists in OBS
@@ -480,16 +458,6 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ -f "distro/opensuse/$PACKAGE.spec" ]];
if [[ "$PACKAGE" == *"-git" ]]; then if [[ "$PACKAGE" == *"-git" ]]; then
update_opensuse_git_spec "$WORK_DIR/$PACKAGE.spec" update_opensuse_git_spec "$WORK_DIR/$PACKAGE.spec"
elif [[ "$PACKAGE" == "dms-greeter" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then
DMS_GREETER_BASE_VERSION=$(echo "$CHANGELOG_VERSION" | sed -E 's/^([0-9]+(\.[0-9]+)*).*/\1/')
DMS_GREETER_RELEASE=$(echo "$CHANGELOG_VERSION" | sed -E 's/.*db([0-9]+)$/\1/' || echo "1")
CHANGELOG_DATE=$(date '+%a %b %d %Y')
sed -i "s/VERSION_PLACEHOLDER/${DMS_GREETER_BASE_VERSION}/g" "$WORK_DIR/$PACKAGE.spec"
sed -i "s/RELEASE_PLACEHOLDER/${DMS_GREETER_RELEASE}/g" "$WORK_DIR/$PACKAGE.spec"
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" "$WORK_DIR/$PACKAGE.spec"
# Explicitly set Version:/Release: in case the spec uses %{version} macro
sed -i "s/^Version:.*/Version: ${DMS_GREETER_BASE_VERSION}/" "$WORK_DIR/$PACKAGE.spec"
sed -i "s/^Release:.*/Release: ${DMS_GREETER_RELEASE}%{?dist}/" "$WORK_DIR/$PACKAGE.spec"
fi fi
if [[ -f "$WORK_DIR/.osc/$PACKAGE.spec" ]]; then if [[ -f "$WORK_DIR/.osc/$PACKAGE.spec" ]]; then
@@ -555,24 +523,6 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ "$UPLOAD_DEBIAN" == false ]] && [[ -f
fi fi
fi fi
# For dms-greeter: download dms-qml.tar.gz from _service URL
if [[ -z "${SOURCE_DIR:-}" ]] && [[ "$PACKAGE" == "dms-greeter" ]] && [[ -f "distro/debian/$PACKAGE/_service" ]]; then
DMS_GREETER_URL=$(grep -A 5 'name="download_url"' "distro/debian/$PACKAGE/_service" | grep "path" | sed 's/.*<param name="path">\(.*\)<\/param>.*/\1/' | head -1)
if [[ -n "$DMS_GREETER_URL" ]]; then
DMS_GREETER_FULL_URL="https://github.com${DMS_GREETER_URL}"
echo " Downloading dms-greeter source from: $DMS_GREETER_FULL_URL"
if wget -q -O "$TEMP_DIR/dms-qml.tar.gz" "$DMS_GREETER_FULL_URL" 2>/dev/null || \
curl -L -f -s -o "$TEMP_DIR/dms-qml.tar.gz" "$DMS_GREETER_FULL_URL" 2>/dev/null; then
cd "$TEMP_DIR"
tar -xzf dms-qml.tar.gz
if [[ -f "Modules/Greetd/assets/dms-greeter" ]]; then
SOURCE_DIR="$TEMP_DIR"
fi
cd "$REPO_ROOT"
fi
fi
fi
if [[ -n "$SOURCE_DIR" && -d "$SOURCE_DIR" ]]; then if [[ -n "$SOURCE_DIR" && -d "$SOURCE_DIR" ]]; then
SOURCE0=$(grep "^Source0:" "distro/opensuse/$PACKAGE.spec" | awk '{print $2}' | head -1) SOURCE0=$(grep "^Source0:" "distro/opensuse/$PACKAGE.spec" | awk '{print $2}' | head -1)
@@ -581,15 +531,6 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ "$UPLOAD_DEBIAN" == false ]] && [[ -f
cd "$OBS_TARBALL_DIR" cd "$OBS_TARBALL_DIR"
case "$PACKAGE" in case "$PACKAGE" in
dms-greeter)
EXPECTED_DIR="dms-qml"
echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)"
mkdir -p "$EXPECTED_DIR"
cp -a "$SOURCE_DIR"/. "$EXPECTED_DIR/"
tar -czhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
rm -rf "$EXPECTED_DIR"
echo " Created $SOURCE0 ($(stat -c%s "$WORK_DIR/$SOURCE0" 2>/dev/null || echo 0) bytes)"
;;
dms) dms)
DMS_VERSION=$(grep "^Version:" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec" | sed 's/^Version:[[:space:]]*//' | head -1) DMS_VERSION=$(grep "^Version:" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec" | sed 's/^Version:[[:space:]]*//' | head -1)
EXPECTED_DIR="DankMaterialShell-${DMS_VERSION}" EXPECTED_DIR="DankMaterialShell-${DMS_VERSION}"
@@ -727,17 +668,6 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
if [[ -z "$SOURCE_DIR" ]]; then if [[ -z "$SOURCE_DIR" ]]; then
SOURCE_DIR=$(find . -maxdepth 1 -type d ! -name "." | head -1) SOURCE_DIR=$(find . -maxdepth 1 -type d ! -name "." | head -1)
fi fi
# dms-qml.tar.gz extracts flat (quickshell contents, no top-level dir)
# Create dms-qml wrapper so combined tarball has correct top-level dir (like dms)
if [[ "$PACKAGE" == "dms-greeter" ]] && [[ -f "Modules/Greetd/assets/dms-greeter" ]]; then
mkdir -p dms-qml
for f in *; do
if [[ -e "$f" && "$f" != "dms-qml" && "$f" != "source-archive" ]]; then
mv "$f" dms-qml/ 2>/dev/null || true
fi
done
SOURCE_DIR="dms-qml"
fi
if [[ -z "$SOURCE_DIR" || ! -d "$SOURCE_DIR" ]]; then if [[ -z "$SOURCE_DIR" || ! -d "$SOURCE_DIR" ]]; then
echo "Error: Failed to extract source archive or find source directory" echo "Error: Failed to extract source archive or find source directory"
echo "Contents of $TEMP_DIR:" echo "Contents of $TEMP_DIR:"
@@ -814,21 +744,6 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
cd "$OBS_TARBALL_DIR" cd "$OBS_TARBALL_DIR"
case "$PACKAGE" in case "$PACKAGE" in
dms-greeter)
EXPECTED_DIR="dms-qml"
echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)"
mkdir -p "$EXPECTED_DIR"
cp -a "$SOURCE_DIR"/. "$EXPECTED_DIR/"
if [[ "$SOURCE0" == *.tar.xz ]]; then
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
elif [[ "$SOURCE0" == *.tar.bz2 ]]; then
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
else
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
fi
rm -rf "$EXPECTED_DIR"
echo " Created $SOURCE0 ($(stat -c%s "$WORK_DIR/$SOURCE0" 2>/dev/null || echo 0) bytes)"
;;
dms) dms)
DMS_VERSION=$(grep "^Version:" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec" | sed 's/^Version:[[:space:]]*//' | head -1) DMS_VERSION=$(grep "^Version:" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec" | sed 's/^Version:[[:space:]]*//' | head -1)
EXPECTED_DIR="DankMaterialShell-${DMS_VERSION}" EXPECTED_DIR="DankMaterialShell-${DMS_VERSION}"
@@ -888,16 +803,6 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
cp "distro/opensuse/$PACKAGE.spec" "$WORK_DIR/" cp "distro/opensuse/$PACKAGE.spec" "$WORK_DIR/"
if [[ "$PACKAGE" == *"-git" ]]; then if [[ "$PACKAGE" == *"-git" ]]; then
update_opensuse_git_spec "$WORK_DIR/$PACKAGE.spec" update_opensuse_git_spec "$WORK_DIR/$PACKAGE.spec"
elif [[ "$PACKAGE" == "dms-greeter" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then
DMS_GREETER_BASE_VERSION=$(echo "$CHANGELOG_VERSION" | sed -E 's/^([0-9]+(\.[0-9]+)*).*/\1/')
DMS_GREETER_RELEASE=$(echo "$CHANGELOG_VERSION" | sed -E 's/.*db([0-9]+)$/\1/' || echo "1")
CHANGELOG_DATE=$(date '+%a %b %d %Y')
sed -i "s/VERSION_PLACEHOLDER/${DMS_GREETER_BASE_VERSION}/g" "$WORK_DIR/$PACKAGE.spec"
sed -i "s/RELEASE_PLACEHOLDER/${DMS_GREETER_RELEASE}/g" "$WORK_DIR/$PACKAGE.spec"
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" "$WORK_DIR/$PACKAGE.spec"
# Explicitly set Version:/Release: in case the spec uses %{version} macro
sed -i "s/^Version:.*/Version: ${DMS_GREETER_BASE_VERSION}/" "$WORK_DIR/$PACKAGE.spec"
sed -i "s/^Release:.*/Release: ${DMS_GREETER_RELEASE}%{?dist}/" "$WORK_DIR/$PACKAGE.spec"
fi fi
fi fi
@@ -1029,47 +934,10 @@ EOF
echo " - Quilt format detected: creating debian.tar.gz" echo " - Quilt format detected: creating debian.tar.gz"
tar -czf "$WORK_DIR/debian.tar.gz" -C "distro/debian/$PACKAGE" debian/ tar -czf "$WORK_DIR/debian.tar.gz" -C "distro/debian/$PACKAGE" debian/
# For dms-greeter: create orig tarball so Debian build gets upstream (OBS only passes .dsc Files to Debian)
DSC_FILES_DEBIAN=""
if [[ "$PACKAGE" == "dms-greeter" ]]; then
UPSTREAM_VER=$(echo "$VERSION" | sed 's/-[^-]*$//')
ORIG_TARBALL="${PACKAGE}_${UPSTREAM_VER}.orig.tar.gz"
ORIG_DIR="${PACKAGE}-${UPSTREAM_VER}"
if [[ -f "distro/debian/$PACKAGE/_service" ]] && grep -q "download_url" "distro/debian/$PACKAGE/_service"; then
DG_TEMP=$(mktemp -d)
DMS_GREETER_PATH=$(grep -A 5 'name="download_url"' "distro/debian/$PACKAGE/_service" | grep "path" | sed 's/.*<param name="path">\(.*\)<\/param>.*/\1/' | head -1)
if [[ -n "$DMS_GREETER_PATH" ]]; then
DG_URL="https://github.com${DMS_GREETER_PATH}"
echo " - Downloading dms-greeter source for orig tarball: $DG_URL"
if wget -q -O "$DG_TEMP/dms-qml.tar.gz" "$DG_URL" 2>/dev/null || curl -L -f -s -o "$DG_TEMP/dms-qml.tar.gz" "$DG_URL" 2>/dev/null; then
( cd "$DG_TEMP" && tar --no-same-owner -xzf dms-qml.tar.gz && mkdir -p "$ORIG_DIR" && \
for f in *; do [[ "$f" != "dms-qml.tar.gz" && "$f" != "$ORIG_DIR" ]] && mv "$f" "$ORIG_DIR/"; done )
if [[ -d "$DG_TEMP/$ORIG_DIR/Modules" ]] || [[ -f "$DG_TEMP/$ORIG_DIR/LICENSE" ]]; then
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$ORIG_TARBALL" -C "$DG_TEMP" "$ORIG_DIR"
ORIG_MD5=$(md5sum "$WORK_DIR/$ORIG_TARBALL" | cut -d' ' -f1)
ORIG_SIZE=$(stat -c%s "$WORK_DIR/$ORIG_TARBALL" 2>/dev/null || stat -f%z "$WORK_DIR/$ORIG_TARBALL" 2>/dev/null)
DSC_FILES_DEBIAN=" $ORIG_MD5 $ORIG_SIZE $ORIG_TARBALL
"
echo " - Created $ORIG_TARBALL for Debian orig"
fi
rm -rf "$DG_TEMP"
fi
fi
fi
fi
DEBIAN_MD5=$(md5sum "$WORK_DIR/debian.tar.gz" | cut -d' ' -f1) DEBIAN_MD5=$(md5sum "$WORK_DIR/debian.tar.gz" | cut -d' ' -f1)
DEBIAN_SIZE=$(stat -c%s "$WORK_DIR/debian.tar.gz" 2>/dev/null || stat -f%z "$WORK_DIR/debian.tar.gz" 2>/dev/null) DEBIAN_SIZE=$(stat -c%s "$WORK_DIR/debian.tar.gz" 2>/dev/null || stat -f%z "$WORK_DIR/debian.tar.gz" 2>/dev/null)
echo " - Generating $PACKAGE.dsc for quilt format" echo " - Generating $PACKAGE.dsc for quilt format"
# debtransform: DEBTRANSFORM-TAR = orig (upstream), DEBTRANSFORM-FILES-TAR = debian archive
DEBTRANSFORM_EXTRA=""
if [[ -n "$DSC_FILES_DEBIAN" ]] && [[ -n "$ORIG_TARBALL" ]]; then
DEBTRANSFORM_EXTRA="DEBTRANSFORM-TAR: $ORIG_TARBALL
DEBTRANSFORM-FILES-TAR: debian.tar.gz
"
fi
cat >"$WORK_DIR/$PACKAGE.dsc" <<DSCEOF cat >"$WORK_DIR/$PACKAGE.dsc" <<DSCEOF
Format: 3.0 (quilt) Format: 3.0 (quilt)
Source: $PACKAGE Source: $PACKAGE
@@ -1078,7 +946,7 @@ Architecture: any
Version: $VERSION Version: $VERSION
Maintainer: Avenge Media <AvengeMedia.US@gmail.com> Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
Build-Depends: debhelper-compat (= 13), wget, gzip Build-Depends: debhelper-compat (= 13), wget, gzip
${DEBTRANSFORM_EXTRA}Files:${DSC_FILES_DEBIAN} Files:
$DEBIAN_MD5 $DEBIAN_SIZE debian.tar.gz $DEBIAN_MD5 $DEBIAN_SIZE debian.tar.gz
DSCEOF DSCEOF
fi fi
@@ -1117,13 +985,6 @@ if [[ -n "$OBS_FILES" ]]; then
continue continue
fi fi
# Keep current orig tarball for dms-greeter (Debian 3.0 quilt needs it)
UPSTREAM_VER_CLEAN=$(echo "$CHANGELOG_VERSION" | sed 's/-[^-]*$//' 2>/dev/null)
if [[ "$PACKAGE" == "dms-greeter" ]] && [[ "$old_file" == "${PACKAGE}_${UPSTREAM_VER_CLEAN}.orig.tar.gz" ]]; then
echo " - Keeping orig tarball: $old_file"
continue
fi
if [[ "$old_file" == "${PACKAGE}-source.tar.gz" ]]; then if [[ "$old_file" == "${PACKAGE}-source.tar.gz" ]]; then
echo " - Keeping source tarball: $old_file" echo " - Keeping source tarball: $old_file"
continue continue
-16
View File
@@ -118,7 +118,6 @@ get_ppa_name() {
case "$pkg" in case "$pkg" in
dms) echo "dms" ;; dms) echo "dms" ;;
dms-git) echo "dms-git" ;; dms-git) echo "dms-git" ;;
dms-greeter) echo "danklinux" ;;
*) echo "" ;; *) echo "" ;;
esac esac
} }
@@ -227,9 +226,6 @@ dms-git)
dms) dms)
GIT_REPO="AvengeMedia/DankMaterialShell" GIT_REPO="AvengeMedia/DankMaterialShell"
;; ;;
dms-greeter)
GIT_REPO="AvengeMedia/DankMaterialShell"
;;
danksearch) danksearch)
GIT_REPO="AvengeMedia/danksearch" GIT_REPO="AvengeMedia/danksearch"
;; ;;
@@ -352,18 +348,6 @@ EOF
fi fi
fi fi
;; ;;
dms-greeter)
info "Downloading source for dms-greeter..."
if [ ! -f "dms-greeter-source.tar.gz" ]; then
info "Downloading dms-greeter source..."
if wget -O dms-greeter-source.tar.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-source.tar.gz"; then
success "source tarball downloaded"
else
error "Failed to download dms-greeter-source.tar.gz"
exit 1
fi
fi
;;
esac esac
fi fi
+2 -3
View File
@@ -14,7 +14,7 @@ LAUNCHPAD_API="https://api.launchpad.net/1.0"
DISTRO_SERIES_LIST=(resolute stonking) DISTRO_SERIES_LIST=(resolute stonking)
# Define packages (sync with ppa-upload.sh) # Define packages (sync with ppa-upload.sh)
ALL_PACKAGES=(dms dms-git dms-greeter) ALL_PACKAGES=(dms dms-git)
# Function to get PPA name for a package # Function to get PPA name for a package
get_ppa_name() { get_ppa_name() {
@@ -22,7 +22,6 @@ get_ppa_name() {
case "$pkg" in case "$pkg" in
dms) echo "dms" ;; dms) echo "dms" ;;
dms-git) echo "dms-git" ;; dms-git) echo "dms-git" ;;
dms-greeter) echo "danklinux" ;;
*) echo "" ;; *) echo "" ;;
esac esac
} }
@@ -72,7 +71,7 @@ elif [[ -n "$PPA_INPUT" ]]; then
else else
# Check all packages in all PPAs # Check all packages in all PPAs
PACKAGES=("${ALL_PACKAGES[@]}") PACKAGES=("${ALL_PACKAGES[@]}")
PPAS=("dms" "dms-git" "danklinux") PPAS=("dms" "dms-git")
fi fi
# Function to get build status color and symbol # Function to get build status color and symbol
-1
View File
@@ -13,7 +13,6 @@ JSON=false
PACKAGES=( PACKAGES=(
"dms:dms:release" "dms:dms:release"
"dms-git:dms-git:git" "dms-git:dms-git:git"
"dms-greeter:danklinux:release"
) )
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
+4 -12
View File
@@ -27,7 +27,7 @@ success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; } error() { echo -e "${RED}[ERROR]${NC} $1"; }
AVAILABLE_PACKAGES=(dms dms-git dms-greeter) AVAILABLE_PACKAGES=(dms dms-git)
KEEP_BUILDS=false KEEP_BUILDS=false
REBUILD_RELEASE="" REBUILD_RELEASE=""
@@ -101,7 +101,6 @@ get_ppa_name() {
case "$pkg" in case "$pkg" in
dms) echo "dms" ;; dms) echo "dms" ;;
dms-git) echo "dms-git" ;; dms-git) echo "dms-git" ;;
dms-greeter) echo "danklinux" ;;
*) echo "" ;; *) echo "" ;;
esac esac
} }
@@ -336,7 +335,7 @@ echo
info "Step 2: Uploading to PPA..." info "Step 2: Uploading to PPA..."
if [ "$PPA_NAME" = "danklinux" ] || [ "$PPA_NAME" = "dms" ] || [ "$PPA_NAME" = "dms-git" ]; then if [ "$PPA_NAME" = "dms" ] || [ "$PPA_NAME" = "dms-git" ]; then
warn "Using lftp for upload" warn "Using lftp for upload"
BUILD_DIR=$(dirname "$CHANGES_FILE") BUILD_DIR=$(dirname "$CHANGES_FILE")
@@ -410,9 +409,9 @@ EOF
fi fi
else else
# This branch should not be reached for DMS packages # This branch should not be reached for DMS packages
# All DMS packages (dms, dms-git, dms-greeter) use lftp # All DMS packages (dms, dms-git) use lftp
error "Unknown PPA: $PPA_NAME" error "Unknown PPA: $PPA_NAME"
error "DMS packages use lftp for upload. Supported PPAs: dms, dms-git, danklinux" error "DMS packages use lftp for upload. Supported PPAs: dms, dms-git"
exit 1 exit 1
fi fi
@@ -484,13 +483,6 @@ if [ "$KEEP_BUILDS" = "false" ]; then
REMOVED=$((REMOVED + 1)) REMOVED=$((REMOVED + 1))
fi fi
;; ;;
dms-greeter)
# Remove downloaded source
if [ -f "$PACKAGE_DIR/dms-greeter-source.tar.gz" ]; then
rm -f "$PACKAGE_DIR/dms-greeter-source.tar.gz"
REMOVED=$((REMOVED + 1))
fi
;;
esac esac
if [ $REMOVED -gt 0 ]; then if [ $REMOVED -gt 0 ]; then
@@ -1,5 +0,0 @@
dms-greeter (1.0.2ppa2) questing; urgency=medium
* Rebuild for packaging fixes (ppa2)
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 06:50:44 +0000
-23
View File
@@ -1,23 +0,0 @@
Source: dms-greeter
Section: x11
Priority: optional
Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
Build-Depends: debhelper-compat (= 13)
Standards-Version: 4.6.2
Homepage: https://github.com/AvengeMedia/DankMaterialShell
Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell
Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
Package: dms-greeter
Architecture: any
Depends: ${misc:Depends},
greetd,
quickshell-git | quickshell
Suggests: niri | hyprland | sway
Description: DankMaterialShell greeter for greetd
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
inspired greeter interface built with Quickshell for Wayland compositors.
.
Supports multiple compositors including Niri, Hyprland, and Sway with automatic
compositor detection and configuration. Features session selection, user
authentication, and dynamic theming.
@@ -1,27 +0,0 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: dms-greeter
Upstream-Contact: Avenge Media LLC <AvengeMedia.US@gmail.com>
Source: https://github.com/AvengeMedia/DankMaterialShell
Files: *
Copyright: 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.
-108
View File
@@ -1,108 +0,0 @@
#!/bin/sh
set -e
case "$1" in
configure)
# Create greeter user/group if they don't exist
if ! getent group greeter >/dev/null; then
addgroup --system greeter
fi
if ! getent passwd greeter >/dev/null; then
adduser --system --ingroup greeter --home /var/lib/greeter \
--shell /bin/bash --gecos "System Greeter" greeter
fi
if [ -d /var/cache/dms-greeter ]; then
chown -R greeter:greeter /var/cache/dms-greeter 2>/dev/null || true
fi
if [ -d /var/lib/greeter ]; then
chown -R greeter:greeter /var/lib/greeter 2>/dev/null || true
fi
# Check and set graphical.target as default
CURRENT_TARGET=$(systemctl get-default 2>/dev/null || echo "unknown")
if [ "$CURRENT_TARGET" != "graphical.target" ]; then
systemctl set-default graphical.target >/dev/null 2>&1 || true
TARGET_STATUS="Set to graphical.target (was: $CURRENT_TARGET) ✓"
else
TARGET_STATUS="Already graphical.target ✓"
fi
GREETD_CONFIG="/etc/greetd/config.toml"
CONFIG_STATUS="Not modified (already configured)"
# Check if niri or hyprland exists
COMPOSITOR="niri"
if ! command -v niri >/dev/null 2>&1; then
if command -v Hyprland >/dev/null 2>&1; then
COMPOSITOR="hyprland"
fi
fi
# If config doesn't exist, create a default one
if [ ! -f "$GREETD_CONFIG" ]; then
mkdir -p /etc/greetd
cat > "$GREETD_CONFIG" << 'GREETD_EOF'
[terminal]
vt = 1
[default_session]
user = "greeter"
command = "/usr/bin/dms-greeter --command COMPOSITOR_PLACEHOLDER"
GREETD_EOF
sed -i "s|COMPOSITOR_PLACEHOLDER|$COMPOSITOR|" "$GREETD_CONFIG"
CONFIG_STATUS="Created new config with $COMPOSITOR ✓"
elif ! grep -q "dms-greeter" "$GREETD_CONFIG"; then
# Backup existing config
BACKUP_FILE="${GREETD_CONFIG}.backup-$(date +%Y%m%d-%H%M%S)"
cp "$GREETD_CONFIG" "$BACKUP_FILE" 2>/dev/null || true
# Update command in default_session section
sed -i "/^\[default_session\]/,/^\[/ s|^command =.*|command = \"/usr/bin/dms-greeter --command $COMPOSITOR\"|" "$GREETD_CONFIG"
sed -i '/^\[default_session\]/,/^\[/ s|^user =.*|user = "greeter"|' "$GREETD_CONFIG"
CONFIG_STATUS="Updated existing config (backed up) with $COMPOSITOR ✓"
fi
# Only show banner on initial install
if [ -z "$2" ]; then
cat << 'EOF'
=========================================================================
DMS Greeter Installation Complete!
=========================================================================
Status:
EOF
echo " ✓ Greetd config: $CONFIG_STATUS"
echo " ✓ Default target: $TARGET_STATUS"
cat << 'EOF'
✓ Greeter user: Created
✓ Greeter directories: /var/cache/dms-greeter, /var/lib/greeter
Next steps:
1. Enable the greeter:
dms greeter enable
(This will automatically disable conflicting display managers,
set graphical.target, and enable greetd)
2. Sync your theme with the greeter (optional):
dms greeter sync
3. Check your setup:
dms greeter status
Ready to test? Run: sudo systemctl start greetd
Documentation: https://danklinux.com/docs/dankgreeter/
=========================================================================
EOF
fi
;;
esac
#DEBHELPER#
exit 0
-14
View File
@@ -1,14 +0,0 @@
#!/bin/sh
set -e
case "$1" in
purge)
# Remove greeter cache directory on purge
rm -rf /var/cache/dms-greeter 2>/dev/null || true
;;
esac
#DEBHELPER#
exit 0
-67
View File
@@ -1,67 +0,0 @@
#!/usr/bin/make -f
export DH_VERBOSE = 1
# Extract version from debian/changelog
DEB_VERSION := $(shell dpkg-parsechangelog -S Version)
# Get upstream version (strip -1ppa1 suffix)
UPSTREAM_VERSION := $(shell echo $(DEB_VERSION) | sed 's/-[^-]*$$//')
BASE_VERSION := $(shell echo $(UPSTREAM_VERSION) | sed 's/ppa[0-9]*$$//' | sed 's/+git.*//')
%:
dh $@
override_dh_auto_build:
# All files are included in source package
test -f dms-greeter-source.tar.gz || (echo "ERROR: dms-greeter-source.tar.gz not found!" && exit 1)
# Extract source tarball
tar -xzf dms-greeter-source.tar.gz
# Find the extracted directory
SOURCE_DIR=$$(find . -maxdepth 1 -type d -name "DankMaterialShell*" | head -n1); \
if [ -n "$$SOURCE_DIR" ]; then \
ln -sf $$SOURCE_DIR DankMaterialShell-$(BASE_VERSION); \
fi
override_dh_auto_install:
# Install greeter files to shared data location
mkdir -p debian/dms-greeter/usr/share/quickshell/dms-greeter
cp -rL DankMaterialShell-$(BASE_VERSION)/quickshell/* debian/dms-greeter/usr/share/quickshell/dms-greeter/
# Install launcher script
install -Dm755 DankMaterialShell-$(BASE_VERSION)/quickshell/Modules/Greetd/assets/dms-greeter \
debian/dms-greeter/usr/bin/dms-greeter
# Install documentation
install -Dm644 DankMaterialShell-$(BASE_VERSION)/quickshell/Modules/Greetd/README.md \
debian/dms-greeter/usr/share/doc/dms-greeter/README.md
# Install LICENSE file
install -Dm644 DankMaterialShell-$(BASE_VERSION)/LICENSE \
debian/dms-greeter/usr/share/doc/dms-greeter/LICENSE
# Install systemd tmpfiles/sysusers fragments only when present in the fetched source.
# sysusers-dms-greeter.conf landed upstream after v1.4.6; guarding both lets older
# release tarballs build, while future tags that ship the files install them automatically.
if [ -f DankMaterialShell-$(BASE_VERSION)/quickshell/systemd/tmpfiles-dms-greeter.conf ]; then \
install -Dpm0644 DankMaterialShell-$(BASE_VERSION)/quickshell/systemd/tmpfiles-dms-greeter.conf \
debian/dms-greeter/usr/lib/tmpfiles.d/dms-greeter.conf; \
fi
if [ -f DankMaterialShell-$(BASE_VERSION)/quickshell/systemd/sysusers-dms-greeter.conf ]; then \
install -Dm644 DankMaterialShell-$(BASE_VERSION)/quickshell/systemd/sysusers-dms-greeter.conf \
debian/dms-greeter/usr/lib/sysusers.d/dms-greeter.conf; \
fi
# Create cache directory structure (will be created by postinst)
mkdir -p debian/dms-greeter/var/cache/dms-greeter
# Remove build and development files
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/core
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/distro
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/.git*
rm -f debian/dms-greeter/usr/share/quickshell/dms-greeter/.gitignore
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/.github
override_dh_auto_clean:
rm -rf DankMaterialShell-*
dh_auto_clean
@@ -1 +0,0 @@
3.0 (native)
@@ -1 +0,0 @@
dms-greeter-source.tar.gz
@@ -1,3 +0,0 @@
# Include files that are normally excluded by .gitignore
# These are needed for the build process on Launchpad (which has no internet access)
tar-ignore = !dms-greeter-source.tar.gz
-25
View File
@@ -5,7 +5,6 @@ XBPS templates for DankMaterialShell on [Void Linux](https://voidlinux.org).
| Package | Source repo | Template | | Package | Source repo | Template |
| --- | --- | --- | | --- | --- | --- |
| `dms` | DankMaterialShell | [`srcpkgs/dms/template`](srcpkgs/dms/template) | | `dms` | DankMaterialShell | [`srcpkgs/dms/template`](srcpkgs/dms/template) |
| `dms-greeter` (optional) | DankMaterialShell | [`srcpkgs/dms-greeter/template`](srcpkgs/dms-greeter/template) |
| `dgop` | AvengeMedia/dgop | maintained in the **danklinux** repo (`distro/void/srcpkgs/dgop`) | | `dgop` | AvengeMedia/dgop | maintained in the **danklinux** repo (`distro/void/srcpkgs/dgop`) |
| `danksearch` | AvengeMedia/danksearch | maintained in the **danklinux** repo (`distro/void/srcpkgs/danksearch`) | | `danksearch` | AvengeMedia/danksearch | maintained in the **danklinux** repo (`distro/void/srcpkgs/danksearch`) |
@@ -83,7 +82,6 @@ Inside a `void-packages` checkout (symlink or copy these `srcpkgs/<pkg>` dirs in
./xbps-src pkg dgop ./xbps-src pkg dgop
./xbps-src pkg danksearch ./xbps-src pkg danksearch
./xbps-src pkg dms ./xbps-src pkg dms
./xbps-src pkg dms-greeter # optional
# lint (xlint ships in the xtools package) # lint (xlint ships in the xtools package)
xlint srcpkgs/dms/template xlint srcpkgs/dms/template
@@ -125,26 +123,3 @@ sudo ln -sf /etc/sv/elogind /var/service/elogind
``` ```
The `dankinstall` Void path enables both services after installing packages. The `dankinstall` Void path enables both services after installing packages.
## Greeter (optional)
Install `dms-greeter`, then let the CLI do the setup:
```sh
dms greeter enable # configures greetd + the Void seat/PAM bits below
dms greeter sync # optional: share theming with the shell
```
`dms-greeter` requires D-Bus and elogind. `dms greeter enable` enables the
`dbus` and `elogind` runit services, configures greetd for elogind
(`LIBSEAT_BACKEND=logind`), adds `_greeter` to the `video` and `input` groups,
and adds `pam_rundir` to `/etc/pam.d/greetd` (so the post-login session gets an
`XDG_RUNTIME_DIR`). It disables seatd if enabled: on Void, seatd is the
alternative to elogind, and running both fights over the seat. Greeter sessions
are launched through `dbus-run-session`. A Wayland compositor and a working DRM
device (`/dev/dri/card*`) are required and not pulled in automatically.
`dms greeter enable` also rewrites `/etc/sv/greetd/run` to wait for the `dbus`
and `elogind` services, preventing a first-boot race that can leave the greeter
on a black screen. greetd package updates restore the stock run script; re-run
`dms greeter enable` afterwards.
@@ -1,15 +0,0 @@
dms-greeter installed.
Configure and enable it with:
dms greeter enable
This points greetd at the greeter and sets up everything Void needs that logind
would handle on systemd: enables seatd, adds the greeter user to the seat/video/
input groups, and adds pam_rundir to the greetd PAM stack. Optionally sync your
shell theme into the greeter with:
dms greeter sync
Requirements not pulled in automatically: a Wayland compositor (niri, hyprland,
sway, …) and a working DRM device (/dev/dri/card*; in a VM, enable virtio-gpu).
-36
View File
@@ -1,36 +0,0 @@
# Template file for 'dms-greeter'
#
# greetd greeter for DankMaterialShell
# Builds from the same DMS release tarball as 'dms'; keep version/checksum in sync.
# CI rewrites version/checksum from the selected release tag before building.
# Setup is done by `dms greeter enable`, not by this package — see distro/void/README.md.
pkgname=dms-greeter
version=1.5.0
revision=2
short_desc="DankMaterialShell greeter for greetd"
maintainer="AvengeMedia <AvengeMedia.US@gmail.com>"
license="MIT"
homepage="https://danklinux.com"
distfiles="https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${version}/dms-source.tar.gz"
checksum=14f2678d6a15223ba069d1b8c8a21a5564b735d190c231f62ed44fd8bf48677c
depends="greetd quickshell acl-progs dbus elogind pam_rundir mesa-dri"
# Cache dir the greeter uses as $HOME (owned by greetd's _greeter user).
make_dirs="/var/cache/dms-greeter 0750 _greeter _greeter"
do_install() {
# Launcher wrapper -> /usr/bin/dms-greeter
vbin quickshell/Modules/Greetd/assets/dms-greeter
# Same QML tree as the shell; greeter mode is selected at runtime via DMS_RUN_GREETER.
vmkdir usr/share/quickshell/dms-greeter
vcopy "quickshell/*" usr/share/quickshell/dms-greeter
# Sample compositor configs for reference
vinstall quickshell/Modules/Greetd/assets/dms-niri.kdl 644 usr/share/dms-greeter
vinstall quickshell/Modules/Greetd/assets/dms-hypr.conf 644 usr/share/dms-greeter
vdoc quickshell/Modules/Greetd/README.md
vlicense LICENSE
}
Generated
+2 -2
View File
@@ -3,11 +3,11 @@
"dank-qml-common": { "dank-qml-common": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1784471445, "lastModified": 1784485083,
"narHash": "sha256-hRTmcd3Ms9DF3X35ZiapXM15rG5X1DtIdlrQZwRINFE=", "narHash": "sha256-hRTmcd3Ms9DF3X35ZiapXM15rG5X1DtIdlrQZwRINFE=",
"owner": "AvengeMedia", "owner": "AvengeMedia",
"repo": "dank-qml-common", "repo": "dank-qml-common",
"rev": "493b609cc04659f1388f5738e8f65d6c531994fe", "rev": "c0f2d837e1c9933e6789eeb38e7d456088bd5d1b",
"type": "github" "type": "github"
}, },
"original": { "original": {
+1 -4
View File
@@ -159,9 +159,6 @@
--replace-fail /usr/bin/dms $out/bin/dms \ --replace-fail /usr/bin/dms $out/bin/dms \
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill --replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \ substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so \ --replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so \
--replace-fail pam_deny.so ${pkgs.pam}/lib/security/pam_deny.so \ --replace-fail pam_deny.so ${pkgs.pam}/lib/security/pam_deny.so \
@@ -222,7 +219,7 @@
nixosModules.default = self.nixosModules.dank-material-shell; nixosModules.default = self.nixosModules.dank-material-shell;
nixosModules.greeter = mkModuleWithDmsPkgs ./distro/nix/greeter.nix; nixosModules.greeter = builtins.warn "dank-material-shell: the greeter moved to the dank-greeter repo; use `inputs.dank-greeter.nixosModules.default` and `programs.dms-greeter` (https://github.com/AvengeMedia/dank-greeter)" { };
nixosModules.dankMaterialShell = builtins.warn "dank-material-shell: flake output `nixosModules.dankMaterialShell` has been renamed to `nixosModules.dank-material-shell`" self.nixosModules.dank-material-shell; nixosModules.dankMaterialShell = builtins.warn "dank-material-shell: flake output `nixosModules.dankMaterialShell` has been renamed to `nixosModules.dank-material-shell`" self.nixosModules.dank-material-shell;
+3 -11
View File
@@ -13,8 +13,6 @@ Singleton {
readonly property int cacheConfigVersion: 1 readonly property int cacheConfigVersion: 1
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericCacheLocation) readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericCacheLocation)
readonly property string _stateDir: Paths.strip(_stateUrl) readonly property string _stateDir: Paths.strip(_stateUrl)
@@ -75,10 +73,8 @@ Singleton {
}) })
Component.onCompleted: { Component.onCompleted: {
if (!isGreeterMode) {
loadCache(); loadCache();
} }
}
function loadCache() { function loadCache() {
_loading = true; _loading = true;
@@ -200,7 +196,7 @@ Singleton {
FileView { FileView {
id: launcherCacheFile id: launcherCacheFile
path: isGreeterMode ? "" : _stateDir + "/DankMaterialShell/launcher_cache.json" path: _stateDir + "/DankMaterialShell/launcher_cache.json"
blockLoading: true blockLoading: true
blockWrites: true blockWrites: true
atomicWrites: true atomicWrites: true
@@ -210,20 +206,16 @@ Singleton {
FileView { FileView {
id: cacheFile id: cacheFile
path: isGreeterMode ? "" : _stateDir + "/DankMaterialShell/cache.json" path: _stateDir + "/DankMaterialShell/cache.json"
blockLoading: true blockLoading: true
blockWrites: true blockWrites: true
atomicWrites: true atomicWrites: true
watchChanges: !isGreeterMode watchChanges: true
onLoaded: { onLoaded: {
if (!isGreeterMode) {
parseCache(cacheFile.text()); parseCache(cacheFile.text());
} }
}
onLoadFailed: error => { onLoadFailed: error => {
if (!isGreeterMode) {
log.info("No cache file found, starting fresh"); log.info("No cache file found, starting fresh");
} }
} }
}
} }
-107
View File
@@ -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();
}
+5 -48
View File
@@ -16,7 +16,6 @@ Singleton {
readonly property int sessionConfigVersion: 3 readonly property int sessionConfigVersion: 3
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
property bool _parseError: false property bool _parseError: false
property bool _hasLoaded: false property bool _hasLoaded: false
property bool _isReadOnly: false property bool _isReadOnly: false
@@ -227,10 +226,8 @@ Singleton {
property string settingsSidebarCollapsedIds: "," property string settingsSidebarCollapsedIds: ","
Component.onCompleted: { Component.onCompleted: {
if (!isGreeterMode) {
loadSettings(); loadSettings();
} }
}
property var _pendingMigration: null property var _pendingMigration: null
@@ -238,11 +235,6 @@ Singleton {
_hasUnsavedChanges = false; _hasUnsavedChanges = false;
_pendingMigration = null; _pendingMigration = null;
if (isGreeterMode) {
parseSettings(greeterSessionFile.text());
return;
}
try { try {
const txt = settingsFile.text(); const txt = settingsFile.text();
let obj = (txt && txt.trim()) ? JSON.parse(txt) : null; let obj = (txt && txt.trim()) ? JSON.parse(txt) : null;
@@ -280,7 +272,7 @@ Singleton {
_loadedSessionSnapshot = getCurrentSessionJson(); _loadedSessionSnapshot = getCurrentSessionJson();
_hasLoaded = true; _hasLoaded = true;
if (!isGreeterMode && typeof Theme !== "undefined") if (typeof Theme !== "undefined")
Theme.generateSystemThemesFromCurrentTheme(); Theme.generateSystemThemesFromCurrentTheme();
if (typeof WallpaperCyclingService !== "undefined") if (typeof WallpaperCyclingService !== "undefined")
@@ -362,7 +354,7 @@ Singleton {
_loadedSessionSnapshot = getCurrentSessionJson(); _loadedSessionSnapshot = getCurrentSessionJson();
_hasLoaded = true; _hasLoaded = true;
if (!isGreeterMode && typeof Theme !== "undefined") if (typeof Theme !== "undefined")
Theme.generateSystemThemesFromCurrentTheme(); Theme.generateSystemThemesFromCurrentTheme();
if (typeof WallpaperCyclingService !== "undefined") if (typeof WallpaperCyclingService !== "undefined")
@@ -386,7 +378,7 @@ Singleton {
} }
function saveSettings() { function saveSettings() {
if (isGreeterMode || _parseError || !_hasLoaded) if (_parseError || !_hasLoaded)
return; return;
settingsFile.setText(getCurrentSessionJson()); settingsFile.setText(getCurrentSessionJson());
if (_isReadOnly) if (_isReadOnly)
@@ -1394,56 +1386,21 @@ Singleton {
FileView { FileView {
id: settingsFile id: settingsFile
path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json" path: StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json"
blockLoading: true blockLoading: true
blockWrites: true blockWrites: true
atomicWrites: true atomicWrites: true
watchChanges: !isGreeterMode watchChanges: true
onLoaded: { onLoaded: {
if (!isGreeterMode) {
_hasUnsavedChanges = false; _hasUnsavedChanges = false;
parseSettings(settingsFile.text()); parseSettings(settingsFile.text());
} }
}
onSaveFailed: error => { onSaveFailed: error => {
root._isReadOnly = true; root._isReadOnly = true;
root._hasUnsavedChanges = root._checkForUnsavedChanges(); root._hasUnsavedChanges = root._checkForUnsavedChanges();
} }
} }
readonly property string _greeterCacheDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
property string greeterSessionBaseDir: root._greeterCacheDir
function setGreeterSessionBaseDir(dir) {
const next = dir || root._greeterCacheDir;
if (greeterSessionBaseDir === next)
return;
greeterSessionBaseDir = next;
if (isGreeterMode)
greeterSessionFile.reload();
}
function resetGreeterSessionBaseDir() {
setGreeterSessionBaseDir(root._greeterCacheDir);
}
FileView {
id: greeterSessionFile
path: root.greeterSessionBaseDir ? (root.greeterSessionBaseDir + "/session.json") : ""
preload: isGreeterMode
blockLoading: false
blockWrites: true
watchChanges: false
printErrors: true
onLoaded: {
if (isGreeterMode) {
parseSettings(greeterSessionFile.text());
}
}
}
Process { Process {
id: sessionWritableCheckProcess id: sessionWritableCheckProcess
+3 -25
View File
@@ -17,8 +17,6 @@ Singleton {
readonly property int settingsConfigVersion: 12 readonly property int settingsConfigVersion: 12
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
enum Position { enum Position {
Top, Top,
Bottom, Bottom,
@@ -1439,20 +1437,16 @@ Singleton {
signal workspaceIconsUpdated signal workspaceIconsUpdated
function refreshAuthAvailability() { function refreshAuthAvailability() {
if (isGreeterMode)
return;
Processes.detectAuthCapabilities(); Processes.detectAuthCapabilities();
} }
Component.onCompleted: { Component.onCompleted: {
if (!isGreeterMode) {
Processes.settingsRoot = root; Processes.settingsRoot = root;
loadSettings(); loadSettings();
initializeListModels(); initializeListModels();
refreshAuthAvailability(); refreshAuthAvailability();
Processes.checkPluginSettings(); Processes.checkPluginSettings();
} }
}
function applyStoredTheme() { function applyStoredTheme() {
if (typeof Theme !== "undefined") { if (typeof Theme !== "undefined") {
@@ -1517,8 +1511,6 @@ Singleton {
} }
function checkIconThemeDrift() { function checkIconThemeDrift() {
if (isGreeterMode)
return;
if (resolveIconTheme() === "System Default") if (resolveIconTheme() === "System Default")
return; return;
if (!lastAppliedIconTheme) if (!lastAppliedIconTheme)
@@ -1674,8 +1666,6 @@ Singleton {
} }
function scheduleAuthApply() { function scheduleAuthApply() {
if (isGreeterMode)
return;
Qt.callLater(() => { Qt.callLater(() => {
Processes.settingsRoot = root; Processes.settingsRoot = root;
Processes.scheduleAuthApply(); Processes.scheduleAuthApply();
@@ -1683,8 +1673,6 @@ Singleton {
} }
function scheduleGreeterAutoLoginSync() { function scheduleGreeterAutoLoginSync() {
if (isGreeterMode)
return;
Qt.callLater(() => { Qt.callLater(() => {
Processes.settingsRoot = root; Processes.settingsRoot = root;
Processes.scheduleGreeterAutoLoginSync(); Processes.scheduleGreeterAutoLoginSync();
@@ -1692,8 +1680,6 @@ Singleton {
} }
function markGreeterSyncPending(who, key, oldValue) { function markGreeterSyncPending(who, key, oldValue) {
if (isGreeterMode)
return;
if (!(key in greeterSyncBaseline)) { if (!(key in greeterSyncBaseline)) {
var baseline = greeterSyncBaseline; var baseline = greeterSyncBaseline;
baseline[key] = oldValue; baseline[key] = oldValue;
@@ -3632,7 +3618,7 @@ Singleton {
FileView { FileView {
id: settingsFile id: settingsFile
path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json" path: StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json"
blockLoading: true blockLoading: true
blockWrites: true blockWrites: true
atomicWrites: true atomicWrites: true
@@ -3645,8 +3631,6 @@ Singleton {
settingsFileReloadDebounce.restart(); settingsFileReloadDebounce.restart();
} }
onLoaded: { onLoaded: {
if (isGreeterMode)
return;
const wasLoaded = _hasLoaded; const wasLoaded = _hasLoaded;
const prevFrameEnabled = frameEnabled; const prevFrameEnabled = frameEnabled;
const prevFrameMode = frameMode; const prevFrameMode = frameMode;
@@ -3689,10 +3673,8 @@ Singleton {
updateFrameCompositorLayout(); updateFrameCompositorLayout();
} }
onLoadFailed: error => { onLoadFailed: error => {
if (!isGreeterMode) {
applyStoredTheme(); applyStoredTheme();
} }
}
onSaveFailed: error => { onSaveFailed: error => {
root._isReadOnly = true; root._isReadOnly = true;
root._hasUnsavedChanges = root._checkForUnsavedChanges(); root._hasUnsavedChanges = root._checkForUnsavedChanges();
@@ -3702,26 +3684,22 @@ Singleton {
FileView { FileView {
id: pluginSettingsFile id: pluginSettingsFile
path: isGreeterMode ? "" : pluginSettingsPath path: pluginSettingsPath
blockLoading: true blockLoading: true
blockWrites: true blockWrites: true
atomicWrites: true atomicWrites: true
printErrors: false printErrors: false
watchChanges: !isGreeterMode watchChanges: true
onLoaded: { onLoaded: {
if (!isGreeterMode) {
parsePluginSettings(pluginSettingsFile.text()); parsePluginSettings(pluginSettingsFile.text());
} }
}
onLoadFailed: error => { onLoadFailed: error => {
if (!isGreeterMode) {
const msg = String(error || ""); const msg = String(error || "");
if (!_isMissingPluginSettingsError(error)) if (!_isMissingPluginSettingsError(error))
log.warn("Failed to load plugin_settings.json. Error:", msg); log.warn("Failed to load plugin_settings.json. Error:", msg);
_resetPluginSettings(); _resetPluginSettings();
} }
} }
}
property bool pluginSettingsFileExists: false property bool pluginSettingsFileExists: false
+16 -86
View File
@@ -8,7 +8,6 @@ import Quickshell.Io
import qs.Common import qs.Common
import qs.DankCommon.Common as DankCommon import qs.DankCommon.Common as DankCommon
import qs.Services import qs.Services
import qs.Modules.Greetd
import "StockThemes.js" as StockThemes import "StockThemes.js" as StockThemes
Singleton { Singleton {
@@ -143,13 +142,11 @@ Singleton {
Component.onCompleted: { Component.onCompleted: {
Quickshell.execDetached(["mkdir", "-p", stateDir]); Quickshell.execDetached(["mkdir", "-p", stateDir]);
if (typeof SessionData === "undefined" || !SessionData.isGreeterMode)
Quickshell.execDetached([shellDir + "/scripts/gtk.sh", configDir, "", shellDir, "assets-only"]); Quickshell.execDetached([shellDir + "/scripts/gtk.sh", configDir, "", shellDir, "assets-only"]);
Proc.runCommand("matugenCheck", ["sh", "-c", "command -v matugen"], (output, code) => { Proc.runCommand("matugenCheck", ["sh", "-c", "command -v matugen"], (output, code) => {
matugenAvailable = (code === 0) && !envDisableMatugen; matugenAvailable = (code === 0) && !envDisableMatugen;
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode);
if (!matugenAvailable || isGreeterMode) { if (!matugenAvailable) {
return; return;
} }
@@ -428,13 +425,6 @@ Singleton {
DMSService.sendRequest("theme.auto.trigger", {}); DMSService.sendRequest("theme.auto.trigger", {});
} }
function applyGreeterTheme(themeName) {
switchTheme(themeName, false, false);
if (themeName === dynamic && dynamicColorsFileView.path) {
dynamicColorsFileView.reload();
}
}
function getMatugenColor(path, fallback) { function getMatugenColor(path, fallback) {
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark"; const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark";
let cur = matugenColors && matugenColors.colors && matugenColors.colors[colorMode]; let cur = matugenColors && matugenColors.colors && matugenColors.colors[colorMode];
@@ -1222,26 +1212,11 @@ Singleton {
return presetMap[SettingsData.modalAnimationSpeed] ?? 150; return presetMap[SettingsData.modalAnimationSpeed] ?? 150;
} }
property real cornerRadius: { property real cornerRadius: typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
return GreetdSettings.cornerRadius;
}
return typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12;
}
property string fontFamily: { property string fontFamily: typeof SettingsData !== "undefined" ? resolvedFontFamily(SettingsData.fontFamily) : DankCommon.Fonts.sans
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
return resolvedFontFamily(GreetdSettings.getEffectiveFontFamily());
}
return typeof SettingsData !== "undefined" ? resolvedFontFamily(SettingsData.fontFamily) : DankCommon.Fonts.sans;
}
property string monoFontFamily: { property string monoFontFamily: typeof SettingsData !== "undefined" ? resolvedMonoFontFamily(SettingsData.monoFontFamily) : DankCommon.Fonts.mono
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
return resolvedMonoFontFamily(GreetdSettings.monoFontFamily);
}
return typeof SettingsData !== "undefined" ? resolvedMonoFontFamily(SettingsData.monoFontFamily) : DankCommon.Fonts.mono;
}
function resolvedFontFamily(family) { function resolvedFontFamily(family) {
if (family === defaultFontFamily) if (family === defaultFontFamily)
@@ -1255,19 +1230,9 @@ Singleton {
return family; return family;
} }
property int fontWeight: { property int fontWeight: typeof SettingsData !== "undefined" ? SettingsData.fontWeight : Font.Normal
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
return GreetdSettings.fontWeight;
}
return typeof SettingsData !== "undefined" ? SettingsData.fontWeight : Font.Normal;
}
property real fontScale: { property real fontScale: typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
return GreetdSettings.fontScale;
}
return typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0;
}
property real spacingXXS: 2 property real spacingXXS: 2
property real spacingXS: 4 property real spacingXS: 4
@@ -1327,16 +1292,13 @@ Singleton {
currentThemeCategory = "generic"; currentThemeCategory = "generic";
} }
} }
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode); if (savePrefs && typeof SettingsData !== "undefined") {
if (savePrefs && typeof SettingsData !== "undefined" && !isGreeterMode) {
SettingsData.set("currentThemeCategory", currentThemeCategory); SettingsData.set("currentThemeCategory", currentThemeCategory);
SettingsData.set("currentThemeName", currentTheme); SettingsData.set("currentThemeName", currentTheme);
} }
if (!isGreeterMode) {
generateSystemThemesFromCurrentTheme(); generateSystemThemesFromCurrentTheme();
} }
}
function setLightMode(light, savePrefs = true, enableTransition = false) { function setLightMode(light, savePrefs = true, enableTransition = false) {
if (enableTransition) { if (enableTransition) {
@@ -1347,19 +1309,16 @@ Singleton {
return; return;
} }
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode); if (savePrefs && typeof SessionData !== "undefined") {
if (savePrefs && typeof SessionData !== "undefined" && !isGreeterMode) {
SessionData.setLightMode(light); SessionData.setLightMode(light);
} }
if (!isGreeterMode) {
PortalService.setLightMode(light); PortalService.setLightMode(light);
if (typeof SettingsData !== "undefined") { if (typeof SettingsData !== "undefined") {
SettingsData.updateCosmicThemeMode(light); SettingsData.updateCosmicThemeMode(light);
} }
generateSystemThemesFromCurrentTheme(); generateSystemThemesFromCurrentTheme();
} }
}
function toggleLightMode(savePrefs = true) { function toggleLightMode(savePrefs = true) {
setLightMode(!isLightMode, savePrefs, true); setLightMode(!isLightMode, savePrefs, true);
@@ -1412,8 +1371,7 @@ Singleton {
if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) { if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) {
const defaults = themeData.variants.defaults || {}; const defaults = themeData.variants.defaults || {};
const modeDefaults = defaults[colorMode] || defaults.dark || {}; const modeDefaults = defaults[colorMode] || defaults.dark || {};
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode; const stored = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults, colorMode) : modeDefaults;
const stored = isGreeterMode ? (GreetdSettings.registryThemeVariants[themeId]?.[colorMode] || modeDefaults) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults, colorMode) : modeDefaults);
var flavorId = stored.flavor || modeDefaults.flavor || ""; var flavorId = stored.flavor || modeDefaults.flavor || "";
const accentId = stored.accent || modeDefaults.accent || ""; const accentId = stored.accent || modeDefaults.accent || "";
var flavor = findVariant(themeData.variants.flavors, flavorId); var flavor = findVariant(themeData.variants.flavors, flavorId);
@@ -1439,8 +1397,7 @@ Singleton {
} }
if (themeData.variants.options && themeData.variants.options.length > 0) { if (themeData.variants.options && themeData.variants.options.length > 0) {
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode; const selectedVariantId = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, themeData.variants.default) : themeData.variants.default;
const selectedVariantId = isGreeterMode ? (typeof GreetdSettings.registryThemeVariants[themeId] === "string" ? GreetdSettings.registryThemeVariants[themeId] : themeData.variants.default) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, themeData.variants.default) : themeData.variants.default);
const variant = findVariant(themeData.variants.options, selectedVariantId); const variant = findVariant(themeData.variants.options, selectedVariantId);
if (variant) { if (variant) {
const variantColors = variant[colorMode] || variant.dark || variant.light || {}; const variantColors = variant[colorMode] || variant.dark || variant.light || {};
@@ -1800,8 +1757,7 @@ Singleton {
} }
function generateSystemThemesFromCurrentTheme() { function generateSystemThemesFromCurrentTheme() {
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode); if (!matugenAvailable)
if (!matugenAvailable || isGreeterMode)
return; return;
_lastGenerateMs = Date.now(); _lastGenerateMs = Date.now();
@@ -1842,9 +1798,8 @@ Singleton {
const defaults = customThemeRawData.variants.defaults || {}; const defaults = customThemeRawData.variants.defaults || {};
const darkDefaults = defaults.dark || {}; const darkDefaults = defaults.dark || {};
const lightDefaults = defaults.light || defaults.dark || {}; const lightDefaults = defaults.light || defaults.dark || {};
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode; const storedDark = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults;
const storedDark = isGreeterMode ? (GreetdSettings.registryThemeVariants[themeId]?.dark || darkDefaults) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults); const storedLight = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults;
const storedLight = isGreeterMode ? (GreetdSettings.registryThemeVariants[themeId]?.light || lightDefaults) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults);
const darkFlavorId = storedDark.flavor || darkDefaults.flavor || ""; const darkFlavorId = storedDark.flavor || darkDefaults.flavor || "";
const lightFlavorId = storedLight.flavor || lightDefaults.flavor || ""; const lightFlavorId = storedLight.flavor || lightDefaults.flavor || "";
const darkAccentId = storedDark.accent || darkDefaults.accent || ""; const darkAccentId = storedDark.accent || darkDefaults.accent || "";
@@ -1864,8 +1819,7 @@ Singleton {
lightTheme = mergeColors(lightTheme, lightAccent[lightFlavor.id] || {}); lightTheme = mergeColors(lightTheme, lightAccent[lightFlavor.id] || {});
} }
} else if (customThemeRawData.variants.options) { } else if (customThemeRawData.variants.options) {
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode; const selectedVariantId = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, customThemeRawData.variants.default) : customThemeRawData.variants.default;
const selectedVariantId = isGreeterMode ? (typeof GreetdSettings.registryThemeVariants[themeId] === "string" ? GreetdSettings.registryThemeVariants[themeId] : customThemeRawData.variants.default) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, customThemeRawData.variants.default) : customThemeRawData.variants.default);
const variant = findVariant(customThemeRawData.variants.options, selectedVariantId); const variant = findVariant(customThemeRawData.variants.options, selectedVariantId);
if (variant) { if (variant) {
darkTheme = mergeColors(darkTheme, variant.dark || {}); darkTheme = mergeColors(darkTheme, variant.dark || {});
@@ -2214,32 +2168,11 @@ Singleton {
} }
} }
readonly property string _greeterCacheDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
property string greeterColorsBaseDir: root._greeterCacheDir
function setGreeterColorsBaseDir(dir) {
const next = dir || root._greeterCacheDir;
if (greeterColorsBaseDir === next)
return;
greeterColorsBaseDir = next;
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode)
dynamicColorsFileView.reload();
}
function resetGreeterColorsBaseDir() {
setGreeterColorsBaseDir(root._greeterCacheDir);
}
FileView { FileView {
id: dynamicColorsFileView id: dynamicColorsFileView
path: { path: stateDir + "/dms-colors.json"
if (SessionData.isGreeterMode)
return root.greeterColorsBaseDir ? (root.greeterColorsBaseDir + "/colors.json") : "";
return stateDir + "/dms-colors.json";
}
blockLoading: false blockLoading: false
watchChanges: !SessionData.isGreeterMode watchChanges: true
function parseAndLoadColors() { function parseAndLoadColors() {
try { try {
@@ -2274,9 +2207,6 @@ Singleton {
if (currentTheme !== dynamic) if (currentTheme !== dynamic)
return; return;
if (SessionData.isGreeterMode)
return;
if (workerRunning) { if (workerRunning) {
colorsReloadRetry.restart(); colorsReloadRetry.restart();
return; return;
+6 -6
View File
@@ -13,14 +13,14 @@ Singleton {
property var settingsRoot: null property var settingsRoot: null
onSettingsRootChanged: { onSettingsRootChanged: {
if (settingsRoot && !settingsRoot.isGreeterMode) if (settingsRoot)
consumeGreeterAutoLoginPendingSync(); consumeGreeterAutoLoginPendingSync();
} }
readonly property string greeterAutoLoginPendingSyncPath: (Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter") + "/.local/state/auto-login-sync-pending" readonly property string greeterAutoLoginPendingSyncPath: (Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter") + "/.local/state/auto-login-sync-pending"
function consumeGreeterAutoLoginPendingSync() { function consumeGreeterAutoLoginPendingSync() {
if (!settingsRoot || settingsRoot.isGreeterMode) if (!settingsRoot)
return; return;
greeterAutoLoginPendingCheckProcess.running = true; greeterAutoLoginPendingCheckProcess.running = true;
} }
@@ -287,7 +287,7 @@ Singleton {
property string authApplyTerminalFallbackStderr: "" property string authApplyTerminalFallbackStderr: ""
function scheduleAuthApply() { function scheduleAuthApply() {
if (!settingsRoot || settingsRoot.isGreeterMode) if (!settingsRoot)
return; return;
authApplyQueued = true; authApplyQueued = true;
@@ -300,7 +300,7 @@ Singleton {
} }
function beginAuthApply() { function beginAuthApply() {
if (!authApplyQueued || authApplyRunning || !settingsRoot || settingsRoot.isGreeterMode) if (!authApplyQueued || authApplyRunning || !settingsRoot)
return; return;
authApplyQueued = false; authApplyQueued = false;
@@ -339,7 +339,7 @@ Singleton {
property string greeterAutoLoginSyncStderr: "" property string greeterAutoLoginSyncStderr: ""
function scheduleGreeterAutoLoginSync() { function scheduleGreeterAutoLoginSync() {
if (!settingsRoot || settingsRoot.isGreeterMode) if (!settingsRoot)
return; return;
greeterAutoLoginSyncQueued = true; greeterAutoLoginSyncQueued = true;
@@ -352,7 +352,7 @@ Singleton {
} }
function beginGreeterAutoLoginSync() { function beginGreeterAutoLoginSync() {
if (!greeterAutoLoginSyncQueued || greeterAutoLoginSyncRunning || !settingsRoot || settingsRoot.isGreeterMode) if (!greeterAutoLoginSyncQueued || greeterAutoLoginSyncRunning || !settingsRoot)
return; return;
greeterAutoLoginSyncQueued = false; greeterAutoLoginSyncQueued = false;
-9
View File
@@ -1,9 +0,0 @@
import QtQuick
import Quickshell
import qs.Modules.Greetd
Scope {
id: root
GreeterSurface {}
}
@@ -7,12 +7,7 @@ import qs.Services
Variants { Variants {
readonly property var log: Log.scoped("BlurredWallpaperBackground") readonly property var log: Log.scoped("BlurredWallpaperBackground")
model: { model: SettingsData.getFilteredScreens("wallpaper")
if (SessionData.isGreeterMode) {
return Quickshell.screens;
}
return SettingsData.getFilteredScreens("wallpaper");
}
PanelWindow { PanelWindow {
id: blurWallpaperWindow id: blurWallpaperWindow
+2 -2
View File
@@ -144,7 +144,7 @@ Item {
smooth: true smooth: true
cache: true cache: true
sourceSize: root.blurTextureSize sourceSize: root.blurTextureSize
fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SessionData.getMonitorWallpaperFillMode(root.screenName)) fillMode: root.getFillMode(SessionData.getMonitorWallpaperFillMode(root.screenName))
onStatusChanged: { onStatusChanged: {
if (status === Image.Error) { if (status === Image.Error) {
@@ -166,7 +166,7 @@ Item {
smooth: true smooth: true
cache: true cache: true
sourceSize: root.blurTextureSize sourceSize: root.blurTextureSize
fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SessionData.getMonitorWallpaperFillMode(root.screenName)) fillMode: root.getFillMode(SessionData.getMonitorWallpaperFillMode(root.screenName))
onStatusChanged: { onStatusChanged: {
if (status === Image.Error) { if (status === Image.Error) {
@@ -6,7 +6,7 @@ import qs.Common
import qs.Modules.Plugins import qs.Modules.Plugins
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
import "../../../Common/LayoutCodes.js" as LayoutCodes import "../../../DankCommon/Common/LayoutCodes.js" as LayoutCodes
BasePill { BasePill {
id: root id: root
-20
View File
@@ -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;
}
-162
View File
@@ -1,162 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import "GreetdEnv.js" as GreetdEnv
import qs.Services
Singleton {
id: root
readonly property var log: Log.scoped("GreetdMemory")
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
readonly property string sessionConfigPath: greetCfgDir + "/session.json"
readonly property string memoryFile: greetCfgDir + "/.local/state/memory.json"
readonly property bool rememberLastSession: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], true)
readonly property bool rememberLastUser: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], true)
property string lastSessionId: ""
property string lastSessionDesktopId: ""
property string lastSessionExec: ""
property string lastSuccessfulUser: ""
property bool memoryReady: false
property bool isLightMode: false
property bool nightModeEnabled: false
Component.onCompleted: {
loadMemory();
loadSessionConfig();
}
function loadMemory() {
parseMemory(memoryFileView.text());
}
function loadSessionConfig() {
parseSessionConfig(sessionConfigFileView.text());
}
function parseSessionConfig(content) {
try {
if (content && content.trim()) {
const config = JSON.parse(content);
isLightMode = config.isLightMode !== undefined ? config.isLightMode : false;
nightModeEnabled = config.nightModeEnabled !== undefined ? config.nightModeEnabled : false;
}
} catch (e) {
log.warn("Failed to parse greeter session config:", e);
}
}
function parseMemory(content) {
try {
if (!content || !content.trim())
return;
const memory = JSON.parse(content);
lastSessionId = rememberLastSession ? (memory.lastSessionId || "") : "";
lastSessionDesktopId = rememberLastSession ? (memory.lastSessionDesktopId || "") : "";
lastSessionExec = rememberLastSession ? (memory.lastSessionExec || "") : "";
lastSuccessfulUser = rememberLastUser ? (memory.lastSuccessfulUser || "") : "";
if (!rememberLastSession || !rememberLastUser)
saveMemory();
} catch (e) {
log.warn("Failed to parse greetd memory:", e);
}
}
function saveMemory() {
let memory = {};
if (rememberLastSession && lastSessionId)
memory.lastSessionId = lastSessionId;
if (rememberLastSession && lastSessionDesktopId)
memory.lastSessionDesktopId = lastSessionDesktopId;
if (rememberLastUser && lastSuccessfulUser)
memory.lastSuccessfulUser = lastSuccessfulUser;
memoryFileView.setText(JSON.stringify(memory, null, 2));
}
function setLastSession(id, desktopId) {
if (!rememberLastSession) {
if (lastSessionId !== "" || lastSessionDesktopId !== "" || lastSessionExec !== "") {
lastSessionId = "";
lastSessionDesktopId = "";
lastSessionExec = "";
saveMemory();
}
return;
}
lastSessionId = id || "";
lastSessionDesktopId = desktopId || "";
lastSessionExec = "";
if (!lastSessionId)
lastSessionDesktopId = "";
saveMemory();
}
function setLastSessionId(id) {
setLastSession(id, lastSessionDesktopId);
}
function setLastSessionDesktopId(id) {
setLastSession(lastSessionId, id);
}
function setLastSessionExec(exec) {
if (!rememberLastSession) {
if (lastSessionExec !== "") {
lastSessionExec = "";
saveMemory();
}
return;
}
if (lastSessionExec !== "") {
lastSessionExec = "";
saveMemory();
}
}
function setLastSuccessfulUser(username) {
if (!rememberLastUser) {
if (lastSuccessfulUser !== "") {
lastSuccessfulUser = "";
saveMemory();
}
return;
}
lastSuccessfulUser = username || "";
saveMemory();
}
FileView {
id: memoryFileView
path: root.memoryFile
blockLoading: false
blockWrites: false
atomicWrites: true
watchChanges: false
printErrors: false
onLoaded: {
parseMemory(memoryFileView.text());
root.memoryReady = true;
}
onLoadFailed: {
root.memoryReady = true;
}
}
FileView {
id: sessionConfigFileView
path: root.sessionConfigPath
blockLoading: false
blockWrites: true
atomicWrites: false
watchChanges: false
printErrors: false
onLoaded: {
parseSessionConfig(sessionConfigFileView.text());
}
onLoadFailed: {}
}
}
@@ -1,230 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import "GreetdEnv.js" as GreetdEnv
Singleton {
id: root
readonly property var log: Log.scoped("GreetdSettings")
readonly property string _greeterCacheDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
property string configBaseDir: root._greeterCacheDir
readonly property string configPath: root.configBaseDir ? (root.configBaseDir + "/settings.json") : ""
readonly property string greeterWallpaperOverridePath: root.configBaseDir ? (root.configBaseDir + "/greeter_wallpaper_override.jpg") : ""
function setConfigBaseDir(dir) {
const next = dir || root._greeterCacheDir;
if (configBaseDir === next)
return;
configBaseDir = next;
settingsLoaded = false;
settingsFile.reload();
}
function resetConfigBaseDir() {
setConfigBaseDir(root._greeterCacheDir);
}
property string currentThemeName: "purple"
property bool settingsLoaded: false
property string customThemeFile: ""
property var registryThemeVariants: ({})
property string matugenScheme: "scheme-tonal-spot"
property string clockFormat: "auto"
readonly property bool localeUses24Hour: {
const fmt = Qt.locale().timeFormat(Locale.ShortFormat).replace(/'[^']*'/g, "");
return !/[aA]/.test(fmt);
}
readonly property bool use24HourClock: clockFormat === "24h" ? true : (clockFormat === "12h" ? false : localeUses24Hour)
property bool showSeconds: false
property bool padHours12Hour: false
property string greeterLockDateFormat: ""
property string greeterFontFamily: ""
property string greeterWallpaperFillMode: ""
property bool useFahrenheit: false
property bool nightModeEnabled: false
property string weatherLocation: "New York, NY"
property string weatherCoordinates: "40.7128,-74.0060"
property bool useAutoLocation: false
property bool weatherEnabled: true
property string iconTheme: "System Default"
property bool useOSLogo: false
property string osLogoColorOverride: ""
property real osLogoBrightness: 0.5
property real osLogoContrast: 1
property string fontFamily: "Inter Variable"
property string monoFontFamily: "Fira Code"
property int fontWeight: Font.Normal
property real fontScale: 1.0
property real cornerRadius: 12
property string widgetBackgroundColor: "sch"
property string lockDateFormat: ""
property bool lockScreenShowPowerActions: true
property bool lockScreenShowProfileImage: true
property bool rememberLastSession: true
property bool rememberLastUser: true
property bool greeterAutoLogin: false
property bool greeterEnableFprint: false
property bool greeterEnableU2f: false
property string greeterWallpaperPath: ""
property bool powerActionConfirm: true
property real powerActionHoldDuration: 0.5
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
property string powerMenuDefaultAction: "logout"
property bool powerMenuGridLayout: false
property var screenPreferences: ({})
property int animationSpeed: 2
property string wallpaperFillMode: "Fill"
property string wallpaperBackgroundColorMode: "black"
property string wallpaperBackgroundCustomColor: "#000000"
readonly property color effectiveWallpaperBackgroundColor: {
switch (wallpaperBackgroundColorMode) {
case "black":
return "#000000";
case "white":
return "#ffffff";
case "primary":
return (typeof Theme !== "undefined") ? Theme.primary : "#000000";
case "surface":
return (typeof Theme !== "undefined") ? Theme.surfaceContainer : "#000000";
case "custom":
return wallpaperBackgroundCustomColor;
default:
return "#000000";
}
}
function parseSettings(content) {
try {
let settings = {};
if (content && content.trim()) {
settings = JSON.parse(content);
}
const envRememberLastSession = GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], undefined);
const envRememberLastUser = GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], undefined);
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "purple";
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "";
registryThemeVariants = settings.registryThemeVariants !== undefined ? settings.registryThemeVariants : ({});
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot";
clockFormat = settings.clockFormat !== undefined ? settings.clockFormat : (settings.use24HourClock !== undefined ? (settings.use24HourClock ? "24h" : "12h") : "auto");
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : false;
greeterLockDateFormat = settings.greeterLockDateFormat !== undefined ? settings.greeterLockDateFormat : "";
greeterFontFamily = settings.greeterFontFamily !== undefined ? settings.greeterFontFamily : "";
greeterWallpaperFillMode = settings.greeterWallpaperFillMode !== undefined ? settings.greeterWallpaperFillMode : "";
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
weatherCoordinates = settings.weatherCoordinates !== undefined ? settings.weatherCoordinates : "40.7128,-74.0060";
useAutoLocation = settings.useAutoLocation !== undefined ? settings.useAutoLocation : false;
weatherEnabled = settings.weatherEnabled !== undefined ? settings.weatherEnabled : true;
iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default";
useOSLogo = settings.useOSLogo !== undefined ? settings.useOSLogo : false;
osLogoColorOverride = settings.osLogoColorOverride !== undefined ? settings.osLogoColorOverride : "";
osLogoBrightness = settings.osLogoBrightness !== undefined ? settings.osLogoBrightness : 0.5;
osLogoContrast = settings.osLogoContrast !== undefined ? settings.osLogoContrast : 1;
fontFamily = settings.fontFamily !== undefined ? settings.fontFamily : Theme.defaultFontFamily;
monoFontFamily = settings.monoFontFamily !== undefined ? settings.monoFontFamily : Theme.defaultMonoFontFamily;
fontWeight = settings.fontWeight !== undefined ? settings.fontWeight : Font.Normal;
fontScale = settings.fontScale !== undefined ? settings.fontScale : 1.0;
cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12;
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch";
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : "";
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true;
lockScreenShowProfileImage = settings.lockScreenShowProfileImage !== undefined ? settings.lockScreenShowProfileImage : true;
if (envRememberLastSession !== undefined) {
rememberLastSession = envRememberLastSession;
} else {
rememberLastSession = settings.greeterRememberLastSession !== undefined ? settings.greeterRememberLastSession : settings.rememberLastSession !== undefined ? settings.rememberLastSession : true;
}
if (envRememberLastUser !== undefined) {
rememberLastUser = envRememberLastUser;
} else {
rememberLastUser = settings.greeterRememberLastUser !== undefined ? settings.greeterRememberLastUser : settings.rememberLastUser !== undefined ? settings.rememberLastUser : true;
}
if (configBaseDir === root._greeterCacheDir) {
greeterAutoLogin = settings.greeterAutoLogin !== undefined ? settings.greeterAutoLogin : false;
}
greeterEnableFprint = settings.greeterEnableFprint !== undefined ? settings.greeterEnableFprint : false;
greeterEnableU2f = settings.greeterEnableU2f !== undefined ? settings.greeterEnableU2f : false;
greeterWallpaperPath = settings.greeterWallpaperPath !== undefined ? settings.greeterWallpaperPath : "";
powerActionConfirm = settings.powerActionConfirm !== undefined ? settings.powerActionConfirm : true;
powerActionHoldDuration = settings.powerActionHoldDuration !== undefined ? settings.powerActionHoldDuration : 0.5;
powerMenuActions = settings.powerMenuActions !== undefined ? settings.powerMenuActions : ["reboot", "logout", "poweroff", "lock", "suspend", "restart"];
powerMenuDefaultAction = settings.powerMenuDefaultAction !== undefined ? settings.powerMenuDefaultAction : "logout";
powerMenuGridLayout = settings.powerMenuGridLayout !== undefined ? settings.powerMenuGridLayout : false;
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({});
animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : 2;
wallpaperFillMode = settings.wallpaperFillMode !== undefined ? settings.wallpaperFillMode : "Fill";
wallpaperBackgroundColorMode = settings.wallpaperBackgroundColorMode !== undefined ? settings.wallpaperBackgroundColorMode : "black";
wallpaperBackgroundCustomColor = settings.wallpaperBackgroundCustomColor !== undefined ? settings.wallpaperBackgroundCustomColor : "#000000";
if (typeof Theme !== "undefined") {
if (currentThemeName === "custom" && customThemeFile) {
Theme.loadCustomThemeFromFile(customThemeFile);
}
Theme.applyGreeterTheme(currentThemeName);
}
} catch (e) {
log.warn("Failed to parse greetd settings:", e);
} finally {
settingsLoaded = true;
}
}
function getEffectiveTimeFormat() {
const use24 = use24HourClock;
const secs = showSeconds;
const pad = padHours12Hour;
if (use24)
return secs ? "hh:mm:ss" : "hh:mm";
if (pad)
return secs ? "hh:mm:ss AP" : "hh:mm AP";
return secs ? "h:mm:ss AP" : "h:mm AP";
}
function getEffectiveLockDateFormat() {
const fmt = (greeterLockDateFormat !== undefined && greeterLockDateFormat !== "") ? greeterLockDateFormat : lockDateFormat;
return fmt && fmt.length > 0 ? fmt : Locale.LongFormat;
}
function getEffectiveWallpaperFillMode() {
return (greeterWallpaperFillMode && greeterWallpaperFillMode !== "") ? greeterWallpaperFillMode : wallpaperFillMode;
}
function getEffectiveFontFamily() {
return (greeterFontFamily && greeterFontFamily !== "") ? greeterFontFamily : fontFamily;
}
function getFilteredScreens(componentId) {
const prefs = screenPreferences && screenPreferences[componentId] || ["all"];
if (prefs.includes("all")) {
return Quickshell.screens;
}
return Quickshell.screens.filter(screen => prefs.includes(screen.name));
}
FileView {
id: settingsFile
path: root.configPath
blockLoading: false
blockWrites: true
atomicWrites: false
watchChanges: false
printErrors: false
onLoaded: {
parseSettings(settingsFile.text());
}
onLoadFailed: {
root.parseSettings("");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,35 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
Singleton {
id: root
property string passwordBuffer: ""
property string username: ""
property string usernameInput: ""
property bool showPasswordInput: false
property string selectedSession: ""
property string selectedSessionPath: ""
property string selectedSessionDesktopId: ""
property string pamState: ""
property bool unlocking: false
property var sessionList: []
property var sessionExecs: []
property var sessionPaths: []
property var sessionDesktopIds: []
property int currentSessionIndex: 0
property var availableUsers: []
property int selectedUserIndex: -1
function reset() {
showPasswordInput = false;
username = "";
usernameInput = "";
passwordBuffer = "";
pamState = "";
selectedUserIndex = -1;
}
}
@@ -1,34 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Greetd
import qs.Common
Variants {
model: Quickshell.screens
PanelWindow {
id: root
property var modelData
screen: modelData
anchors {
left: true
right: true
top: true
bottom: true
}
exclusionMode: ExclusionMode.Normal
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
color: "transparent"
GreeterContent {
anchors.fill: parent
screenName: root.screen?.name ?? ""
}
}
}
@@ -1,240 +0,0 @@
import QtQuick
import QtQuick.Layouts
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property bool expanded: false
property int maxExpandedHeight: 400
property bool autoLoginVisible: false
property bool autoLoginChecked: false
property bool manualEntryVisible: false
signal userSelected(string username)
signal toggleRequested
signal autoLoginToggled
signal manualEntryRequested
readonly property int rowHeight: 52
readonly property int collapsedBarHeight: 36
readonly property int actionRowHeight: 44
readonly property int userListFullHeight: {
const count = GreeterUsersService.users.length;
if (count === 0)
return 0;
return count * rowHeight + Math.max(0, count - 1) * Theme.spacingXS;
}
readonly property int manualEntryBlockHeight: manualEntryVisible ? actionRowHeight + Theme.spacingXS : 0
readonly property int autoLoginBlockHeight: autoLoginVisible ? actionRowHeight + Theme.spacingXS : 0
readonly property int expandedContentHeight: {
if (!expanded)
return 0;
if (GreeterUsersService.users.length === 0 && !autoLoginVisible && !manualEntryVisible)
return 0;
return Math.min(maxExpandedHeight, userListFullHeight + manualEntryBlockHeight + autoLoginBlockHeight);
}
function encodeFileUrl(path) {
if (!path)
return "";
return "file://" + path.split("/").map(s => encodeURIComponent(s)).join("/");
}
function profileImageSource(username) {
const path = GreeterUsersService.profileImagePath(username);
if (path)
return encodeFileUrl(path);
return "";
}
implicitHeight: expanded ? expandedContentHeight : collapsedBarHeight
implicitWidth: parent ? parent.width : 320
Item {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
height: collapsedBarHeight
visible: !expanded
RowLayout {
anchors.fill: parent
spacing: Theme.spacingM
StyledText {
Layout.fillWidth: true
text: GreeterState.username ? GreeterUsersService.optionLabel(GreeterState.username) : I18n.tr("Select user...", "greeter user picker placeholder")
color: GreeterState.username ? Theme.surfaceText : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeMedium
elide: Text.ElideRight
}
DankIcon {
Layout.alignment: Qt.AlignVCenter
name: "expand_more"
size: 20
color: Theme.surfaceVariantText
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.toggleRequested()
}
}
Column {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: root.expandedContentHeight
visible: expanded
spacing: Theme.spacingXS
DankListView {
id: userListView
width: parent.width
height: parent.height - root.manualEntryBlockHeight - root.autoLoginBlockHeight
clip: true
interactive: contentHeight > height
spacing: Theme.spacingXS
model: GreeterUsersService.users
delegate: Rectangle {
id: userRow
required property var modelData
required property int index
width: userListView.width
height: root.rowHeight
radius: Theme.cornerRadius
color: userRowMouse.containsMouse ? Theme.surfacePressed : Theme.withAlpha(Theme.surfacePressed, 0)
border.color: GreeterState.username === userRow.modelData.username ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: GreeterState.username === userRow.modelData.username ? 1 : 0
RowLayout {
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingM
Item {
Layout.preferredWidth: 36
Layout.preferredHeight: 36
DankCircularImage {
anchors.fill: parent
imageSource: root.profileImageSource(userRow.modelData.username)
fallbackIcon: "person"
}
}
StyledText {
Layout.fillWidth: true
text: GreeterUsersService.optionLabel(userRow.modelData.username)
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
elide: Text.ElideRight
}
}
MouseArea {
id: userRowMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.userSelected(userRow.modelData.username)
}
}
}
Rectangle {
width: parent.width
height: root.actionRowHeight
visible: root.manualEntryVisible
radius: Theme.cornerRadius
color: manualEntryRowMouse.containsMouse ? Theme.surfacePressed : Theme.withAlpha(Theme.surfacePressed, 0)
RowLayout {
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingM
DankIcon {
Layout.alignment: Qt.AlignVCenter
name: "person_add"
size: 20
color: Theme.surfaceVariantText
}
StyledText {
Layout.fillWidth: true
text: I18n.tr("Not listed?", "greeter link to switch to manual username entry")
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
elide: Text.ElideRight
}
}
MouseArea {
id: manualEntryRowMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.manualEntryRequested()
}
}
Rectangle {
width: parent.width
height: root.actionRowHeight
visible: root.autoLoginVisible
radius: Theme.cornerRadius
color: autoLoginRowMouse.containsMouse ? Theme.surfacePressed : Theme.withAlpha(Theme.surfacePressed, 0)
RowLayout {
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingM
DankIcon {
Layout.alignment: Qt.AlignVCenter
name: root.autoLoginChecked ? "check_box" : "check_box_outline_blank"
size: 20
color: root.autoLoginChecked ? Theme.primary : Theme.surfaceVariantText
}
StyledText {
Layout.fillWidth: true
text: I18n.tr("Auto-login")
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
elide: Text.ElideRight
}
}
MouseArea {
id: autoLoginRowMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.autoLoginToggled()
}
}
}
}
@@ -1,51 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
import qs.Services
Singleton {
id: root
readonly property var log: Log.scoped("GreeterUserTheme")
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
property string activeUsername: ""
function userCacheDir(username) {
if (!username)
return "";
return greetCfgDir + "/users/" + username;
}
function applyForUser(username) {
const name = (username || "").trim();
activeUsername = name;
if (!name) {
applyDefault();
return;
}
const dir = userCacheDir(name);
if (typeof GreeterUsersService !== "undefined" && GreeterUsersService.hasSyncedTheme(name)) {
Theme.setGreeterColorsBaseDir(dir);
SessionData.setGreeterSessionBaseDir(dir);
GreetdSettings.setConfigBaseDir(dir);
return;
}
applyDefault();
}
function applyDefault() {
activeUsername = "";
Theme.resetGreeterColorsBaseDir();
SessionData.resetGreeterSessionBaseDir();
GreetdSettings.resetConfigBaseDir();
}
readonly property string activeWallpaperOverridePath: {
const base = activeUsername && typeof GreeterUsersService !== "undefined" && GreeterUsersService.hasSyncedTheme(activeUsername) ? userCacheDir(activeUsername) : greetCfgDir;
return base ? base + "/greeter_wallpaper_override.jpg" : "";
}
}
-320
View File
@@ -1,320 +0,0 @@
# Dank (dms) Greeter
A greeter for [greetd](https://github.com/kennylevinsen/greetd) that follows the aesthetics of the dms lock screen.
## Features
- **Multi user**: Login with any system user
- **dms sync**: Sync settings with dms for consistent styling between shell and greeter
- **Multiple compositors**: The `dms-greeter` wrapper supports niri, Hyprland, sway, scroll, miracle-wm, labwc, and mangowc.
- **Custom PAM**: Supports custom PAM configuration in `/etc/pam.d/greetd`
- **Session Memory**: Remembers last selected session and user
- Can be disabled via `settings.json` keys: `greeterRememberLastSession` and `greeterRememberLastUser`
## Installation
### Arch Linux
Arch linux users can install [greetd-dms-greeter-git](https://aur.archlinux.org/packages/greetd-dms-greeter-git) from the AUR.
```bash
paru -S greetd-dms-greeter-git
# Or with yay
yay -S greetd-dms-greeter-git
```
### Debian / openSUSE
Official packages are available from the [DankLinux OBS repository](https://software.opensuse.org/download/package?package=dms-greeter&project=home%3AAvengeMedia%3Adanklinux). Add the repo for your distribution and install:
```bash
# Debian 13
sudo apt install dms-greeter # after adding the repo
# openSUSE Tumbleweed
zypper install dms-greeter # after adding the repo
```
See the [Installation guide](https://danklinux.com/docs/dankgreeter/installation) for full repository setup.
If you previously installed manually, remove legacy files first:
```bash
sudo rm -f /usr/local/bin/dms-greeter
sudo rm -rf /etc/xdg/quickshell/dms-greeter
```
Then complete setup:
```bash
dms greeter enable
dms greeter sync
```
Fingerprint authentication is optional. Install your distribution's
fingerprint service/client and PAM integration before enabling it. Package
names vary by distribution; on Fedora the native stack is:
```bash
sudo dnf install fprintd fprintd-pam
fprintd-enroll
```
The distro `fprintd` package normally pulls in `libfprint` automatically. After
enrollment, enable fingerprint login in **Settings → Greeter**. The toggle
applies the shared PAM configuration automatically, so no additional sync is
needed after initial greeter setup. Use `dms auth sync` only to repair or apply
authentication manually without syncing the rest of the greeter.
greetd tries fingerprint and password sequentially. DMS-managed PAM allows two
scans for up to ten seconds before password fallback. Existing distro PAM
policy takes precedence, so fallback timing can differ.
Verify enrollment with `fprintd-list "$USER"`; pass a token such as
`fprintd-enroll -f right-thumb` to enroll a specific finger. Security keys
similarly require `pam_u2f` setup and key registration before enabling the
login toggle.
If `fprintd-enroll` reports **No devices available**, check the [libfprint
supported devices list](https://fprint.freedesktop.org/supported-devices.html).
Some Validity/Synaptics readers are not supported by the native stack,
regardless of distribution; the
[open-fprintd](https://github.com/uunicorn/open-fprintd) service with the
[python-validity](https://github.com/uunicorn/python-validity) driver may work
instead. Follow the driver's installation and firmware instructions, and do
not run the stock `fprintd` daemon alongside its replacement.
#### Syncing themes (Optional)
To sync your wallpaper and theme with the greeter login screen, follow the manual setup below:
##### Manual theme syncing
```bash
# Add yourself to greeter group
sudo usermod -aG greeter <username>
# Set ACLs to allow greeter to traverse your directories
setfacl -m u:greeter:x ~ ~/.config ~/.local ~/.cache ~/.local/state
# Set group ownership on config directories
sudo chgrp -R greeter ~/.config/DankMaterialShell
sudo chgrp -R greeter ~/.local/state/DankMaterialShell
sudo chgrp -R greeter ~/.cache/DankMaterialShell
sudo chmod -R g+rX ~/.config/DankMaterialShell ~/.cache/DankMaterialShell ~/.cache/quickshell
# Create symlinks
sudo ln -sf ~/.config/DankMaterialShell/settings.json /var/cache/dms-greeter/settings.json
sudo ln -sf ~/.local/state/DankMaterialShell/session.json /var/cache/dms-greeter/session.json
sudo ln -sf ~/.cache/DankMaterialShell/dms-colors.json /var/cache/dms-greeter/colors.json
# Logout and login for group membership to take effect
```
</details>
### Fedora / RHEL / Rocky / Alma
Install from COPR or build the RPM:
```bash
# From COPR (when available)
sudo dnf copr enable avenge/dms
sudo dnf install dms-greeter
# Or build locally
cd /path/to/DankMaterialShell
rpkg local
sudo rpm -ivh x86_64/dms-greeter-*.rpm
```
The package automatically:
- Creates the greeter user (via `systemd-sysusers` from `/usr/lib/sysusers.d/dms-greeter.conf` for atomic/immutable compatibility, with package script fallback)
- Sets up directories and permissions
- Configures greetd with auto-detected compositor
- Applies SELinux contexts
Then complete setup:
```bash
dms greeter enable
dms greeter sync
```
#### Syncing themes (Optional)
Run:
```bash
dms greeter sync
```
Then logout/login to see your wallpaper on the greeter.
### Automatic
The easiest thing is to run `dms greeter install` or `dms` for interactive installation.
On Debian/openSUSE, this now prefers the `dms-greeter` package when the OBS repo is configured.
### Manual (fallback only)
Use this only if no package is available for your distro.
1. Install `greetd` (in most distro's standard repositories) and `quickshell`
2. Create the greeter user (if not already created by greetd):
```bash
sudo groupadd -r greeter
sudo useradd -r -g greeter -d /var/lib/greeter -s /bin/bash -c "System Greeter" greeter
sudo mkdir -p /var/lib/greeter
sudo chown greeter:greeter /var/lib/greeter
```
3. Clone the dms project to `/etc/xdg/quickshell/dms-greeter`:
```bash
sudo git clone https://github.com/AvengeMedia/DankMaterialShell.git /etc/xdg/quickshell/dms-greeter
```
4. Copy `Modules/Greetd/assets/dms-greeter` to `/usr/local/bin/dms-greeter`:
```bash
sudo cp /etc/xdg/quickshell/dms-greeter/Modules/Greetd/assets/dms-greeter /usr/local/bin/dms-greeter
sudo chmod +x /usr/local/bin/dms-greeter
```
5. Create greeter cache directory with proper permissions:
```bash
sudo mkdir -p /var/cache/dms-greeter
sudo chown <greeter-user>:<greeter-group> /var/cache/dms-greeter
sudo chmod 2770 /var/cache/dms-greeter
```
6. Edit or create `/etc/greetd/config.toml`:
```toml
[terminal]
vt = 1
[default_session]
user = "greeter"
# Change compositor to another wrapper-supported compositor if preferred
command = "/usr/local/bin/dms-greeter --command niri"
```
7. Disable existing display manager and enable greetd:
```bash
sudo systemctl disable gdm sddm lightdm
sudo systemctl enable greetd
```
8. (Optional) Set up theme syncing using the manual ACL method described in the Configuration → Personalization section below
#### Legacy installation (deprecated)
If you prefer the old method with separate shell scripts and config files:
1. Copy `assets/dms-niri.kdl` or `assets/dms-hypr.lua` (legacy: `assets/dms-hypr.conf`) to `/etc/greetd`
2. Copy `assets/greet-niri.sh` or `assets/greet-hyprland.sh` to `/usr/local/bin/start-dms-greetd.sh`
3. Edit the config file and replace `_DMS_PATH_` with your DMS installation path
4. Configure greetd to use `/usr/local/bin/start-dms-greetd.sh`
### NixOS
To install the greeter on NixOS add the repo to your flake inputs as described in the readme. Then somewhere in your NixOS config add this to imports:
```nix
imports = [
inputs.dank-material-shell.nixosModules.greeter
]
```
Enable the greeter with this in your NixOS config:
```nix
programs.dank-material-shell.greeter = {
enable = true;
compositor.name = "niri"; # or set to hyprland
configHome = "/home/user"; # optionally copyies that users DMS settings (and wallpaper if set) to the greeters data directory as root before greeter starts
};
```
## Usage
### Using dms-greeter wrapper (recommended)
The `dms-greeter` wrapper simplifies running the greeter with any compositor:
```bash
dms-greeter --command niri
dms-greeter --command hyprland
dms-greeter --command sway
dms-greeter --command mangowc
dms-greeter --command niri -C /path/to/custom-niri.kdl
dms-greeter --command niri --remember-last-user false --remember-last-session false
```
Configure greetd to use it in `/etc/greetd/config.toml`:
```toml
[terminal]
vt = 1
[default_session]
user = "greeter"
command = "/usr/bin/dms-greeter --command niri"
```
### Manual usage
To run dms in greeter mode you can also manually set environment variables:
```bash
DMS_RUN_GREETER=1 qs -p /path/to/dms
```
### Configuration
#### Compositor
For current wrapper-based installs, the `dms-greeter` wrapper supports niri, hyprland, sway, scroll, miracle-wm, labwc, and mangowc.
Only niri currently has a generated greeter config path managed by `dms greeter sync`.
- niri: `dms greeter sync` writes the generated greeter config to `/etc/greetd/niri/config.kdl`. Add local manual tweaks in `/etc/greetd/niri_overrides.kdl`.
- Other wrapper-supported compositors use the wrapper-generated config by default. If you need a custom compositor config, add `-C /path/to/config` to the `dms-greeter` command in `/etc/greetd/config.toml`.
#### Personalization
The greeter can be personalized with wallpapers, themes, weather, clock formats, and more - configured exactly the same as dms.
**Easiest method (single user):** Run `dms greeter sync` to automatically sync your DMS theme with the greeter.
**Multi-user systems:** One **main admin** runs full sync once to set up greetd and the shared cache (`dms greeter sync`, or `dms greeter sync --local` when developing from a checkout). **Every other account**—including other admins—should only run:
```bash
dms greeter sync --profile
```
Before that, an administrator must add each user to the `greeter` group in **Settings → Users** (greeter toggle) or with `sudo usermod -aG greeter <username>`. Each added user must log out and back in before `--profile` will work.
Per-user settings are stored under `/var/cache/dms-greeter/users/<username>/` for the login picker; the root cache remains the default fallback and is owned by whoever ran full sync.
**Manual method:** You can manually synchronize configurations if you want greeter settings to always mirror your shell:
```bash
# Add yourself to the greeter group
sudo usermod -aG greeter $USER
# Set ACLs to allow greeter user to traverse your home directory
setfacl -m u:greeter:x ~ ~/.config ~/.local ~/.cache ~/.local/state
# Set group permissions on DMS directories
sudo chgrp -R greeter ~/.config/DankMaterialShell ~/.local/state/DankMaterialShell ~/.cache/quickshell
sudo chmod -R g+rX ~/.config/DankMaterialShell ~/.local/state/DankMaterialShell ~/.cache/quickshell
# Create symlinks for theme files
sudo ln -sf ~/.config/DankMaterialShell/settings.json /var/cache/dms-greeter/settings.json
sudo ln -sf ~/.local/state/DankMaterialShell/session.json /var/cache/dms-greeter/session.json
sudo ln -sf ~/.cache/DankMaterialShell/dms-colors.json /var/cache/dms-greeter/colors.json
# Logout and login for group membership to take effect
```
**Advanced:** You can override the configuration path with the `DMS_GREET_CFG_DIR` environment variable or the `--cache-dir` flag when using `dms-greeter`. The default is `/var/cache/dms-greeter`.
The cache directory should be owned by `<greeter-user>:<greeter-group>` with `2770` permissions. If the greeter user is not available yet, DMS falls back to `root:<greeter-group>`.
@@ -1,499 +0,0 @@
#!/bin/bash
set -e
COMPOSITOR=""
COMPOSITOR_CONFIG=""
DMS_PATH="dms-greeter"
CACHE_DIR="/var/cache/dms-greeter"
REMEMBER_LAST_SESSION=""
REMEMBER_LAST_USER=""
DEBUG_MODE=0
show_help() {
cat << EOF
dms-greeter - DankMaterialShell greeter launcher
Usage: dms-greeter --command COMPOSITOR [OPTIONS]
Required:
--command COMPOSITOR Compositor to use (niri, hyprland, sway, scroll, miracle, mango, or labwc)
Options:
-C, --config PATH Custom compositor config file
-p, --path PATH DMS path (config name or absolute path)
(default: dms-greeter)
--cache-dir PATH Cache directory for greeter data
(default: /var/cache/dms-greeter)
--remember-last-session BOOL
Persist selected session to greeter memory
(BOOL: true/false, default: from settings.json)
--remember-last-user BOOL
Persist last successful username to greeter memory
(BOOL: true/false, default: from settings.json)
--no-save-session Alias for --remember-last-session false
--no-save-username Alias for --remember-last-user false
--debug Enable verbose startup logging to stderr
-h, --help Show this help message
Examples:
dms-greeter --command niri
dms-greeter --command hyprland -C /etc/greetd/custom-hypr.conf
dms-greeter --command sway -p /home/user/.config/quickshell/custom-dms
dms-greeter --command scroll -p /home/user/.config/quickshell/custom-dms
dms-greeter --command niri --cache-dir /tmp/dmsgreeter
dms-greeter --command niri --no-save-session --no-save-username
dms-greeter --command mango
dms-greeter --command labwc
EOF
}
require_command() {
local cmd="$1"
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "Error: required command '$cmd' was not found in PATH" >&2
exit 1
fi
}
normalize_bool_flag() {
local flag_name="$1"
local value="$2"
local normalized="${value,,}"
case "$normalized" in
1|true|yes|on)
echo "1"
;;
0|false|no|off)
echo "0"
;;
*)
echo "Error: $flag_name must be true/false (or 1/0, yes/no, on/off)" >&2
exit 1
;;
esac
}
is_hyprland_lua_config() {
local config_file="$1"
case "$config_file" in
*.lua)
return 0
;;
esac
if [[ -f "$config_file" ]] && grep -qE '(^|[^[:alnum:]_])hl\.' "$config_file"; then
return 0
fi
return 1
}
clear_vt() {
local clear_sequence=$'\033[2J\033[H\033[3J\033[?25l'
# Clear retained console text on the controlling VT.
if printf '%s' "$clear_sequence" >/dev/tty 2>/dev/null; then
return
fi
if [[ -t 1 ]]; then
printf '%s' "$clear_sequence"
fi
}
exec_compositor() {
local log_tag="$1"
shift
if [[ "$DEBUG_MODE" == "1" ]]; then
exec "$@"
fi
clear_vt
if command -v systemd-cat >/dev/null 2>&1; then
exec "$@" > >(systemd-cat -t "dms-greeter/$log_tag" -p info 2>/dev/null) 2>&1
fi
# Preserve diagnostics on systems without journald.
exec "$@" >>"$CACHE_DIR/$log_tag.log" 2>&1
}
while [[ $# -gt 0 ]]; do
case $1 in
--command)
COMPOSITOR="$2"
shift 2
;;
-C|--config)
COMPOSITOR_CONFIG="$2"
shift 2
;;
-p|--path)
DMS_PATH="$2"
shift 2
;;
--cache-dir)
CACHE_DIR="$2"
shift 2
;;
--remember-last-session)
REMEMBER_LAST_SESSION="$2"
shift 2
;;
--remember-last-user)
REMEMBER_LAST_USER="$2"
shift 2
;;
--no-save-session)
REMEMBER_LAST_SESSION="0"
shift
;;
--no-save-username)
REMEMBER_LAST_USER="0"
shift
;;
--debug)
DEBUG_MODE=1
shift
;;
-h|--help)
show_help
exit 0
;;
*)
echo "Unknown option: $1" >&2
show_help
exit 1
;;
esac
done
if [[ -z "$COMPOSITOR" ]]; then
echo "Error: --command COMPOSITOR is required" >&2
show_help
exit 1
fi
locate_dms_config() {
local config_name="$1"
local search_paths=()
local config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
search_paths+=("$config_home/quickshell/$config_name")
search_paths+=("/usr/share/quickshell/$config_name")
local config_dirs="${XDG_CONFIG_DIRS:-/etc/xdg}"
IFS=':' read -ra dirs <<< "$config_dirs"
for dir in "${dirs[@]}"; do
if [[ -n "$dir" ]]; then
search_paths+=("$dir/quickshell/$config_name")
fi
done
for path in "${search_paths[@]}"; do
if [[ -f "$path/shell.qml" ]]; then
echo "$path"
return 0
fi
done
return 1
}
export XDG_SESSION_TYPE=wayland
export QT_QPA_PLATFORM=wayland
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
export EGL_PLATFORM=gbm
export DMS_RUN_GREETER=1
if [[ ! -d "$CACHE_DIR" ]]; then
echo "Error: cache directory '$CACHE_DIR' does not exist." >&2
echo " Run 'dms greeter sync' to initialize it, or pass --cache-dir to an existing directory." >&2
exit 1
fi
export DMS_GREET_CFG_DIR="$CACHE_DIR"
if [[ -n "$REMEMBER_LAST_SESSION" ]]; then
DMS_GREET_REMEMBER_LAST_SESSION=$(normalize_bool_flag "--remember-last-session" "$REMEMBER_LAST_SESSION")
export DMS_GREET_REMEMBER_LAST_SESSION
if [[ "$DMS_GREET_REMEMBER_LAST_SESSION" == "1" ]]; then
DMS_SAVE_SESSION=true
else
DMS_SAVE_SESSION=false
fi
export DMS_SAVE_SESSION
fi
if [[ -n "$REMEMBER_LAST_USER" ]]; then
DMS_GREET_REMEMBER_LAST_USER=$(normalize_bool_flag "--remember-last-user" "$REMEMBER_LAST_USER")
export DMS_GREET_REMEMBER_LAST_USER
if [[ "$DMS_GREET_REMEMBER_LAST_USER" == "1" ]]; then
DMS_SAVE_USERNAME=true
else
DMS_SAVE_USERNAME=false
fi
export DMS_SAVE_USERNAME
fi
export HOME="$CACHE_DIR"
export XDG_STATE_HOME="$CACHE_DIR/.local/state"
export XDG_DATA_HOME="$CACHE_DIR/.local/share"
export XDG_CACHE_HOME="$CACHE_DIR/.cache"
# Fallback runtime dir for systems without logind/pam_rundir (e.g. Void+seatd),
# where Wayland compositors abort with "RuntimeDirNotSet". Guarded so logind
# systems keep their own.
if [[ -z "${XDG_RUNTIME_DIR:-}" || ! -d "${XDG_RUNTIME_DIR:-}" ]]; then
export XDG_RUNTIME_DIR="$CACHE_DIR/run"
mkdir -p "$XDG_RUNTIME_DIR"
chmod 700 "$XDG_RUNTIME_DIR"
fi
# Keep greeter VT clean by default; callers can override via env or --debug.
if [[ -z "${RUST_LOG:-}" ]]; then
export RUST_LOG=warn
fi
if [[ -z "${NIRI_LOG:-}" ]]; then
export NIRI_LOG=warn
fi
# Export the user's configured cursor (from synced settings.json) for the
# compositor and greeter shell, but only if that theme is installed system-wide.
apply_user_cursor() {
command -v jq >/dev/null 2>&1 || return 0
local settings="$DMS_GREET_CFG_DIR/settings.json"
[[ -f "$settings" ]] || return 0
local theme size
theme=$(jq -r '.cursorSettings.theme // empty' "$settings" 2>/dev/null)
size=$(jq -r '.cursorSettings.size // empty' "$settings" 2>/dev/null)
[[ -n "$theme" && "$theme" != "System Default" ]] || return 0
local dirs=()
IFS=':' read -ra data_dirs <<< "${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
local d
for d in "${data_dirs[@]}"; do
[[ -n "$d" ]] && dirs+=("$d/icons")
done
dirs+=(/run/current-system/sw/share/icons /usr/share/icons /usr/local/share/icons)
for d in "${dirs[@]}"; do
if [[ -d "$d/$theme/cursors" ]]; then
export XCURSOR_PATH="$d${XCURSOR_PATH:+:$XCURSOR_PATH}"
export XCURSOR_THEME="$theme"
[[ -n "$size" ]] && export XCURSOR_SIZE="$size"
return 0
fi
done
}
apply_user_cursor
if command -v qs >/dev/null 2>&1; then
QS_BIN="qs"
elif command -v quickshell >/dev/null 2>&1; then
QS_BIN="quickshell"
else
echo "Error: neither 'qs' nor 'quickshell' was found in PATH" >&2
exit 1
fi
QS_CMD="$QS_BIN"
if [[ "$DMS_PATH" == /* ]]; then
QS_CMD="$QS_BIN -p $DMS_PATH"
else
RESOLVED_PATH=$(locate_dms_config "$DMS_PATH")
if [[ $? -eq 0 && -n "$RESOLVED_PATH" ]]; then
if [[ "$DEBUG_MODE" == "1" ]]; then
echo "Located DMS config at: $RESOLVED_PATH" >&2
fi
QS_CMD="$QS_BIN -p $RESOLVED_PATH"
else
echo "Error: Could not find DMS config '$DMS_PATH' (shell.qml) in any valid config path" >&2
exit 1
fi
fi
case "$COMPOSITOR" in
niri)
require_command "niri"
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp)
# base/default config
cat > "$TEMP_CONFIG" << NIRI_EOF
hotkey-overlay {
skip-at-startup
}
environment {
DMS_RUN_GREETER "1"
}
gestures {
hot-corners {
off
}
}
layout {
background-color "#000000"
}
NIRI_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
else
TEMP_CONFIG=$(mktemp)
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
fi
# Append includes if override_files exist
OVERRIDE_FILES=(
/usr/share/greetd/niri_overrides.kdl # for building into a system image
/etc/greetd/niri_overrides.kdl # for local overrides
)
for override_file in "${OVERRIDE_FILES[@]}"; do
if [[ -e "$override_file" ]]; then
# TODO: maybe move to optional=true when Niri Next drops, so we can avoid these checks ;-)
cat >> "$TEMP_CONFIG" << NIRI_EOF
include "$override_file"
NIRI_EOF
fi
done
# Append spawn command
cat >> "$TEMP_CONFIG" << NIRI_EOF
spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation"
NIRI_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
exec_compositor "niri" niri -c "$COMPOSITOR_CONFIG"
;;
hyprland)
if ! command -v start-hyprland >/dev/null 2>&1 && ! command -v Hyprland >/dev/null 2>&1; then
echo "Error: neither 'start-hyprland' nor 'Hyprland' was found in PATH" >&2
exit 1
fi
if [[ -n "$COMPOSITOR_CONFIG" ]] && is_hyprland_lua_config "$COMPOSITOR_CONFIG"; then
TEMP_CONFIG=$(mktemp --suffix=.lua)
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
cat >> "$TEMP_CONFIG" << HYPRLAND_LUA_EOF
hl.on("hyprland.start", function()
hl.exec_cmd('sh -c "$QS_CMD; hyprctl dispatch exit"')
end)
HYPRLAND_LUA_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
elif [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp)
cat > "$TEMP_CONFIG" << HYPRLAND_EOF
env = DMS_RUN_GREETER,1
misc {
disable_hyprland_logo = true
}
exec-once = sh -c "$QS_CMD; hyprctl dispatch exit"
HYPRLAND_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
else
TEMP_CONFIG=$(mktemp)
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
cat >> "$TEMP_CONFIG" << HYPRLAND_EOF
exec-once = sh -c "$QS_CMD; hyprctl dispatch exit"
HYPRLAND_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi
if command -v start-hyprland >/dev/null 2>&1; then
exec_compositor "hyprland" start-hyprland -- --config "$COMPOSITOR_CONFIG"
else
exec_compositor "hyprland" Hyprland -c "$COMPOSITOR_CONFIG"
fi
;;
sway)
require_command "sway"
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp)
cat > "$TEMP_CONFIG" << SWAY_EOF
exec "$QS_CMD; swaymsg exit"
SWAY_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
else
TEMP_CONFIG=$(mktemp)
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
cat >> "$TEMP_CONFIG" << SWAY_EOF
exec "$QS_CMD; swaymsg exit"
SWAY_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi
exec_compositor "sway" sway --unsupported-gpu -c "$COMPOSITOR_CONFIG"
;;
scroll)
require_command "scroll"
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp)
cat > "$TEMP_CONFIG" << SCROLL_EOF
exec "$QS_CMD; scrollmsg exit"
SCROLL_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
else
TEMP_CONFIG=$(mktemp)
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
cat >> "$TEMP_CONFIG" << SCROLL_EOF
exec "$QS_CMD; scrollmsg exit"
SCROLL_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi
exec_compositor "scroll" scroll -c "$COMPOSITOR_CONFIG"
;;
miracle|miracle-wm)
require_command "miracle-wm"
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp)
cat > "$TEMP_CONFIG" << MIRACLE_EOF
exec "$QS_CMD; miraclemsg exit"
MIRACLE_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
else
TEMP_CONFIG=$(mktemp)
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
cat >> "$TEMP_CONFIG" << MIRACLE_EOF
exec "$QS_CMD; miraclemsg exit"
MIRACLE_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi
exec_compositor "miracle" miracle-wm -c "$COMPOSITOR_CONFIG"
;;
labwc)
require_command "labwc"
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
exec_compositor "labwc" labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD"
else
exec_compositor "labwc" labwc --session "$QS_CMD"
fi
;;
mango|mangowc)
require_command "mango"
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
exec_compositor "mango" mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg dispatch quit"
else
exec_compositor "mango" mango -s "$QS_CMD && mmsg dispatch quit"
fi
;;
*)
echo "Error: Unsupported compositor: $COMPOSITOR" >&2
echo "Supported compositors: niri, hyprland, sway, scroll, miracle, mango, labwc" >&2
exit 1
;;
esac
@@ -1,4 +0,0 @@
# Deprecated: greetd expects Hyprland 0.55+ Lua; use `/etc/greetd/dms-hypr.lua` instead.
env = DMS_RUN_GREETER,1
exec = sh -c "qs -p _DMS_PATH_; hyprctl dispatch exit"
@@ -1,8 +0,0 @@
-- Minimal Hyprland (Lua) session for greetd — replace _DMS_PATH_ with your DMS checkout.
-- Copy to `/etc/greetd/dms-hypr.lua` alongside `greet-hyprland.sh`.
hl.env("DMS_RUN_GREETER", "1")
hl.on("hyprland.start", function()
hl.exec_cmd('sh -c "qs -p _DMS_PATH_; hyprctl dispatch exit"')
end)
@@ -1,19 +0,0 @@
hotkey-overlay {
skip-at-startup
}
environment {
DMS_RUN_GREETER "1"
}
spawn-at-startup "sh" "-c" "qs -p _DMS_PATH_; niri msg action quit --skip-confirmation"
gestures {
hot-corners {
off
}
}
layout {
background-color "#000000"
}
@@ -1,11 +0,0 @@
#!/bin/sh
export XDG_SESSION_TYPE=wayland
export QT_QPA_PLATFORM=wayland
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
export EGL_PLATFORM=gbm
if command -v start-hyprland >/dev/null 2>&1; then
exec start-hyprland -- -c /etc/greetd/dms-hypr.lua
else
exec Hyprland -c /etc/greetd/dms-hypr.lua
fi
@@ -1,8 +0,0 @@
#!/bin/sh
export XDG_SESSION_TYPE=wayland
export QT_QPA_PLATFORM=wayland
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
export EGL_PLATFORM=gbm
exec niri -c /etc/greetd/dms-niri.kdl
@@ -1,44 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
DankActionButton {
id: customButtonKeyboard
circular: false
property string text: ""
width: 40
height: 40
property bool isShift: false
color: Theme.surface
property bool isIcon: text === "keyboard_hide" || text === "Backspace" || text === "Enter"
DankIcon {
anchors.centerIn: parent
name: {
if (parent.text === "keyboard_hide")
return "keyboard_hide";
if (parent.text === "Backspace")
return "backspace";
if (parent.text === "Enter")
return "keyboard_return";
return "";
}
size: 20
color: Theme.surfaceText
visible: parent.isIcon
}
StyledText {
id: contentItem
anchors.centerIn: parent
text: parent.text
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeXLarge
font.weight: Font.Normal
visible: !parent.isIcon
}
}
-369
View File
@@ -1,369 +0,0 @@
import QtQuick
import qs.Common
Rectangle {
id: root
property Item target
height: 60 * 5
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
color: Theme.surfaceContainerHigh
signal dismissed
property double rowSpacing: 0.01 * width // horizontal spacing between keyboard
property double columnSpacing: 0.02 * height // vertical spacing between keyboard
property bool shift: false //Boolean for the shift state
property bool symbols: false //Boolean for the symbol state
property double columns: 10 // Number of column
property double rows: 4 // Number of row
property string strShift: '\u2191'
property string strBackspace: "Backspace"
property string strEnter: "Enter"
property string strClose: "keyboard_hide"
property var modelKeyboard: {
"row_1": [
{
"text": 'q',
"symbol": '1',
"width": 1
},
{
"text": 'w',
"symbol": '2',
"width": 1
},
{
"text": 'e',
"symbol": '3',
"width": 1
},
{
"text": 'r',
"symbol": '4',
"width": 1
},
{
"text": 't',
"symbol": '5',
"width": 1
},
{
"text": 'y',
"symbol": '6',
"width": 1
},
{
"text": 'u',
"symbol": '7',
"width": 1
},
{
"text": 'i',
"symbol": '8',
"width": 1
},
{
"text": 'o',
"symbol": '9',
"width": 1
},
{
"text": 'p',
"symbol": '0',
"width": 1
}
],
"row_2": [
{
"text": 'a',
"symbol": '-',
"width": 1
},
{
"text": 's',
"symbol": '/',
"width": 1
},
{
"text": 'd',
"symbol": ':',
"width": 1
},
{
"text": 'f',
"symbol": ';',
"width": 1
},
{
"text": 'g',
"symbol": '(',
"width": 1
},
{
"text": 'h',
"symbol": ')',
"width": 1
},
{
"text": 'j',
"symbol": '€',
"width": 1
},
{
"text": 'k',
"symbol": '&',
"width": 1
},
{
"text": 'l',
"symbol": '@',
"width": 1
}
],
"row_3": [
{
"text": strShift,
"symbol": strShift,
"width": 1.5
},
{
"text": 'z',
"symbol": '.',
"width": 1
},
{
"text": 'x',
"symbol": ',',
"width": 1
},
{
"text": 'c',
"symbol": '?',
"width": 1
},
{
"text": 'v',
"symbol": '!',
"width": 1
},
{
"text": 'b',
"symbol": "'",
"width": 1
},
{
"text": 'n',
"symbol": "%",
"width": 1
},
{
"text": 'm',
"symbol": '"',
"width": 1
},
{
"text": strBackspace,
"symbol": strBackspace,
"width": 1.5
}
],
"row_4": [
{
"text": strClose,
"symbol": strClose,
"width": 1.5
},
{
"text": "123",
"symbol": 'ABC',
"width": 1.5
},
{
"text": ' ',
"symbol": ' ',
"width": 4.5
},
{
"text": '.',
"symbol": '.',
"width": 1
},
{
"text": strEnter,
"symbol": strEnter,
"width": 1.5
}
]
}
//Here is the corresponding table between the ascii and the key event
property var tableKeyEvent: {
"_0": Qt.Key_0,
"_1": Qt.Key_1,
"_2": Qt.Key_2,
"_3": Qt.Key_3,
"_4": Qt.Key_4,
"_5": Qt.Key_5,
"_6": Qt.Key_6,
"_7": Qt.Key_7,
"_8": Qt.Key_8,
"_9": Qt.Key_9,
"_a": Qt.Key_A,
"_b": Qt.Key_B,
"_c": Qt.Key_C,
"_d": Qt.Key_D,
"_e": Qt.Key_E,
"_f": Qt.Key_F,
"_g": Qt.Key_G,
"_h": Qt.Key_H,
"_i": Qt.Key_I,
"_j": Qt.Key_J,
"_k": Qt.Key_K,
"_l": Qt.Key_L,
"_m": Qt.Key_M,
"_n": Qt.Key_N,
"_o": Qt.Key_O,
"_p": Qt.Key_P,
"_q": Qt.Key_Q,
"_r": Qt.Key_R,
"_s": Qt.Key_S,
"_t": Qt.Key_T,
"_u": Qt.Key_U,
"_v": Qt.Key_V,
"_w": Qt.Key_W,
"_x": Qt.Key_X,
"_y": Qt.Key_Y,
"_z": Qt.Key_Z,
"_←": Qt.Key_Backspace,
"_return": Qt.Key_Return,
"_ ": Qt.Key_Space,
"_-": Qt.Key_Minus,
"_/": Qt.Key_Slash,
"_:": Qt.Key_Colon,
"_;": Qt.Key_Semicolon,
"_(": Qt.Key_BracketLeft,
"_)": Qt.Key_BracketRight,
"_€": parseInt("20ac", 16) // I didn't find the appropriate Qt event so I used the hex format
,
"_&": Qt.Key_Ampersand,
"_@": Qt.Key_At,
'_"': Qt.Key_QuoteDbl,
"_.": Qt.Key_Period,
"_,": Qt.Key_Comma,
"_?": Qt.Key_Question,
"_!": Qt.Key_Exclam,
"_'": Qt.Key_Apostrophe,
"_%": Qt.Key_Percent,
"_*": Qt.Key_Asterisk
}
Item {
id: keyboard_container
anchors.left: parent.left
anchors.leftMargin: 5
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: 5
anchors.bottom: parent.bottom
anchors.bottomMargin: 5
//One column which contains 5 rows
Column {
spacing: columnSpacing
Row {
id: row_1
spacing: rowSpacing
Repeater {
model: modelKeyboard["row_1"]
delegate: CustomButtonKeyboard {
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text
width: modelData.width * keyboard_container.width / columns - rowSpacing
height: keyboard_container.height / rows - columnSpacing
onClicked: root.clicked(text)
}
}
}
Row {
id: row_2
spacing: rowSpacing
Repeater {
model: modelKeyboard["row_2"]
delegate: CustomButtonKeyboard {
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text
width: modelData.width * keyboard_container.width / columns - rowSpacing
height: keyboard_container.height / rows - columnSpacing
onClicked: root.clicked(text)
}
}
}
Row {
id: row_3
spacing: rowSpacing
Repeater {
model: modelKeyboard["row_3"]
delegate: CustomButtonKeyboard {
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text
width: modelData.width * keyboard_container.width / columns - rowSpacing
height: keyboard_container.height / rows - columnSpacing
isShift: shift && text === strShift
onClicked: root.clicked(text)
}
}
}
Row {
id: row_4
spacing: rowSpacing
Repeater {
model: modelKeyboard["row_4"]
delegate: CustomButtonKeyboard {
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text
width: modelData.width * keyboard_container.width / columns - rowSpacing
height: keyboard_container.height / rows - columnSpacing
onClicked: root.clicked(text)
}
}
}
}
}
signal clicked(string text)
Connections {
target: root
function onClicked(text) {
if (!keyboard_controller.target)
return;
if (text === strShift) {
root.shift = !root.shift;
} else if (text === '123') {
root.symbols = true;
} else if (text === 'ABC') {
root.symbols = false;
} else if (text === strEnter) {
if (keyboard_controller.target.accepted) {
keyboard_controller.target.accepted();
}
} else if (text === strClose) {
root.dismissed();
} else {
if (text === strBackspace) {
keyboard_controller.target.backspace();
} else {
var charToInsert = root.symbols ? text : (root.shift ? text.toUpperCase() : text);
keyboard_controller.target.insertText(charToInsert);
}
if (root.shift && text !== strShift)
root.shift = false;
}
}
}
}
@@ -1,41 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Services
Item {
id: keyboard_controller
readonly property var log: Log.scoped("KeyboardController")
// reference on the TextInput
property Item target
//Booléan on the state of the keyboard
property bool isKeyboardActive: false
property var rootObject
function show() {
if (!isKeyboardActive && keyboard === null) {
keyboard = keyboardComponent.createObject(keyboard_controller.rootObject);
keyboard.target = keyboard_controller.target;
keyboard.dismissed.connect(hide);
isKeyboardActive = true;
} else
log.debug("The keyboard is already shown");
}
function hide() {
if (isKeyboardActive && keyboard !== null) {
keyboard.destroy();
isKeyboardActive = false;
} else
log.debug("The keyboard is already hidden");
}
// private
property Item keyboard: null
Component {
id: keyboardComponent
Keyboard {}
}
}
-827
View File
@@ -1,827 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property bool isVisible: false
property bool showLogout: true
property int selectedIndex: 0
property int selectedRow: 0
property int selectedCol: 0
property var visibleActions: []
property int gridColumns: 3
property int gridRows: 2
property bool useGridLayout: false
property string holdAction: ""
property int holdActionIndex: -1
property real holdProgress: 0
property bool showHoldHint: false
property var powerActionConfirmOverride: undefined
property var powerActionHoldDurationOverride: undefined
property var powerMenuActionsOverride: undefined
property var powerMenuDefaultActionOverride: undefined
property var powerMenuGridLayoutOverride: undefined
property var requiredActions: []
readonly property bool needsConfirmation: powerActionConfirmOverride !== undefined ? powerActionConfirmOverride : SettingsData.powerActionConfirm
readonly property int holdDurationMs: (powerActionHoldDurationOverride !== undefined ? powerActionHoldDurationOverride : SettingsData.powerActionHoldDuration) * 1000
signal closed
signal switchUserRequested
function updateVisibleActions() {
const allActions = powerMenuActionsOverride !== undefined ? powerMenuActionsOverride : ((typeof SettingsData !== "undefined" && SettingsData.powerMenuActions) ? SettingsData.powerMenuActions : ["logout", "suspend", "hibernate", "reboot", "poweroff"]);
const hibernateSupported = (typeof SessionService !== "undefined" && SessionService.hibernateSupported) || false;
let filtered = allActions.filter(action => {
if (action === "hibernate" && !hibernateSupported)
return false;
if (action === "lock")
return false;
if (action === "restart")
return false;
if (action === "logout" && !showLogout)
return false;
return true;
});
for (const action of requiredActions) {
if (!filtered.includes(action))
filtered.push(action);
}
visibleActions = filtered;
useGridLayout = powerMenuGridLayoutOverride !== undefined ? powerMenuGridLayoutOverride : ((typeof SettingsData !== "undefined" && SettingsData.powerMenuGridLayout !== undefined) ? SettingsData.powerMenuGridLayout : false);
if (!useGridLayout)
return;
const count = visibleActions.length;
if (count === 0) {
gridColumns = 1;
gridRows = 1;
return;
}
if (count <= 3) {
gridColumns = 1;
gridRows = count;
return;
}
if (count === 4) {
gridColumns = 2;
gridRows = 2;
return;
}
gridColumns = 3;
gridRows = Math.ceil(count / 3);
}
function getDefaultActionIndex() {
const defaultAction = powerMenuDefaultActionOverride !== undefined ? powerMenuDefaultActionOverride : ((typeof SettingsData !== "undefined" && SettingsData.powerMenuDefaultAction) ? SettingsData.powerMenuDefaultAction : "suspend");
const index = visibleActions.indexOf(defaultAction);
return index >= 0 ? index : 0;
}
function getActionAtIndex(index) {
if (index < 0 || index >= visibleActions.length)
return "";
return visibleActions[index];
}
function getActionData(action) {
switch (action) {
case "reboot":
return {
"icon": "restart_alt",
"label": I18n.tr("Reboot"),
"key": "R"
};
case "logout":
return {
"icon": "logout",
"label": I18n.tr("Log Out"),
"key": "X"
};
case "poweroff":
return {
"icon": "power_settings_new",
"label": I18n.tr("Power Off"),
"key": "P"
};
case "suspend":
return {
"icon": "bedtime",
"label": I18n.tr("Suspend"),
"key": "S"
};
case "hibernate":
return {
"icon": "ac_unit",
"label": I18n.tr("Hibernate"),
"key": "H"
};
case "switchuser":
return {
"icon": "switch_account",
"label": I18n.tr("Switch User"),
"key": "U"
};
default:
return {
"icon": "help",
"label": action,
"key": "?"
};
}
}
function actionNeedsConfirm(action) {
return action !== "lock" && action !== "restart";
}
function startHold(action, actionIndex) {
if (!needsConfirmation || !actionNeedsConfirm(action)) {
executeAction(action);
return;
}
holdAction = action;
holdActionIndex = actionIndex;
holdProgress = 0;
showHoldHint = false;
holdTimer.start();
}
function cancelHold() {
if (holdAction === "")
return;
const wasHolding = holdProgress > 0;
holdTimer.stop();
if (wasHolding && holdProgress < 1) {
showHoldHint = true;
hintTimer.restart();
}
holdAction = "";
holdActionIndex = -1;
holdProgress = 0;
}
function completeHold() {
if (holdProgress < 1) {
cancelHold();
return;
}
const action = holdAction;
holdTimer.stop();
holdAction = "";
holdActionIndex = -1;
holdProgress = 0;
executeAction(action);
}
function executeAction(action) {
if (!action)
return;
if (action === "switchuser") {
hide();
switchUserRequested();
return;
}
if (typeof SessionService === "undefined")
return;
hide();
switch (action) {
case "logout":
SessionService.logout();
break;
case "suspend":
SessionService.suspend();
break;
case "hibernate":
SessionService.hibernate();
break;
case "reboot":
SessionService.reboot();
break;
case "poweroff":
SessionService.poweroff();
break;
}
}
function selectOption(action, actionIndex) {
startHold(action, actionIndex !== undefined ? actionIndex : -1);
}
function show() {
holdAction = "";
holdActionIndex = -1;
holdProgress = 0;
showHoldHint = false;
updateVisibleActions();
const defaultIndex = getDefaultActionIndex();
if (useGridLayout) {
selectedRow = Math.floor(defaultIndex / gridColumns);
selectedCol = defaultIndex % gridColumns;
selectedIndex = defaultIndex;
} else {
selectedIndex = defaultIndex;
}
isVisible = true;
Qt.callLater(() => powerMenuFocusScope.forceActiveFocus());
}
function hide() {
cancelHold();
isVisible = false;
closed();
}
function handleListNavigation(event, isPressed) {
if (!isPressed) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_X || event.key === Qt.Key_S || event.key === Qt.Key_H || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) {
cancelHold();
event.accepted = true;
}
return;
}
switch (event.key) {
case Qt.Key_Up:
case Qt.Key_Backtab:
selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length;
event.accepted = true;
break;
case Qt.Key_Down:
case Qt.Key_Tab:
selectedIndex = (selectedIndex + 1) % visibleActions.length;
event.accepted = true;
break;
case Qt.Key_Return:
case Qt.Key_Enter:
startHold(getActionAtIndex(selectedIndex), selectedIndex);
event.accepted = true;
break;
case Qt.Key_N:
if (event.modifiers & Qt.ControlModifier) {
selectedIndex = (selectedIndex + 1) % visibleActions.length;
event.accepted = true;
}
break;
case Qt.Key_P:
if (!(event.modifiers & Qt.ControlModifier)) {
if (visibleActions.includes("poweroff")) {
const idx = visibleActions.indexOf("poweroff");
startHold("poweroff", idx);
event.accepted = true;
}
} else {
selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length;
event.accepted = true;
}
break;
case Qt.Key_J:
if (event.modifiers & Qt.ControlModifier) {
selectedIndex = (selectedIndex + 1) % visibleActions.length;
event.accepted = true;
}
break;
case Qt.Key_K:
if (event.modifiers & Qt.ControlModifier) {
selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length;
event.accepted = true;
}
break;
case Qt.Key_R:
if (visibleActions.includes("reboot")) {
startHold("reboot", visibleActions.indexOf("reboot"));
event.accepted = true;
}
break;
case Qt.Key_X:
if (visibleActions.includes("logout")) {
startHold("logout", visibleActions.indexOf("logout"));
event.accepted = true;
}
break;
case Qt.Key_S:
if (visibleActions.includes("suspend")) {
startHold("suspend", visibleActions.indexOf("suspend"));
event.accepted = true;
}
break;
case Qt.Key_H:
if (visibleActions.includes("hibernate")) {
startHold("hibernate", visibleActions.indexOf("hibernate"));
event.accepted = true;
}
break;
}
}
function handleGridNavigation(event, isPressed) {
if (!isPressed) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_X || event.key === Qt.Key_S || event.key === Qt.Key_H || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) {
cancelHold();
event.accepted = true;
}
return;
}
switch (event.key) {
case Qt.Key_Left:
selectedCol = (selectedCol - 1 + gridColumns) % gridColumns;
selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true;
break;
case Qt.Key_Right:
selectedCol = (selectedCol + 1) % gridColumns;
selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true;
break;
case Qt.Key_Up:
case Qt.Key_Backtab:
selectedRow = (selectedRow - 1 + gridRows) % gridRows;
selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true;
break;
case Qt.Key_Down:
case Qt.Key_Tab:
selectedRow = (selectedRow + 1) % gridRows;
selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true;
break;
case Qt.Key_Return:
case Qt.Key_Enter:
startHold(getActionAtIndex(selectedIndex), selectedIndex);
event.accepted = true;
break;
case Qt.Key_N:
if (event.modifiers & Qt.ControlModifier) {
selectedCol = (selectedCol + 1) % gridColumns;
selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true;
}
break;
case Qt.Key_P:
if (!(event.modifiers & Qt.ControlModifier)) {
if (visibleActions.includes("poweroff")) {
const idx = visibleActions.indexOf("poweroff");
startHold("poweroff", idx);
event.accepted = true;
}
} else {
selectedCol = (selectedCol - 1 + gridColumns) % gridColumns;
selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true;
}
break;
case Qt.Key_J:
if (event.modifiers & Qt.ControlModifier) {
selectedRow = (selectedRow + 1) % gridRows;
selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true;
}
break;
case Qt.Key_K:
if (event.modifiers & Qt.ControlModifier) {
selectedRow = (selectedRow - 1 + gridRows) % gridRows;
selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true;
}
break;
case Qt.Key_R:
if (visibleActions.includes("reboot")) {
startHold("reboot", visibleActions.indexOf("reboot"));
event.accepted = true;
}
break;
case Qt.Key_X:
if (visibleActions.includes("logout")) {
startHold("logout", visibleActions.indexOf("logout"));
event.accepted = true;
}
break;
case Qt.Key_S:
if (visibleActions.includes("suspend")) {
startHold("suspend", visibleActions.indexOf("suspend"));
event.accepted = true;
}
break;
case Qt.Key_H:
if (visibleActions.includes("hibernate")) {
startHold("hibernate", visibleActions.indexOf("hibernate"));
event.accepted = true;
}
break;
}
}
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.5)
visible: isVisible
z: 1000
MouseArea {
anchors.fill: parent
onClicked: root.hide()
}
Timer {
id: holdTimer
interval: 16
repeat: true
onTriggered: {
root.holdProgress = Math.min(1, root.holdProgress + (interval / root.holdDurationMs));
if (root.holdProgress >= 1) {
stop();
root.completeHold();
}
}
}
Timer {
id: hintTimer
interval: 2000
onTriggered: root.showHoldHint = false
}
FocusScope {
id: powerMenuFocusScope
anchors.fill: parent
focus: root.isVisible
onVisibleChanged: {
if (visible)
Qt.callLater(() => forceActiveFocus());
}
Keys.onEscapePressed: root.hide()
Keys.onPressed: event => {
if (event.isAutoRepeat) {
event.accepted = true;
return;
}
if (useGridLayout) {
handleGridNavigation(event, true);
} else {
handleListNavigation(event, true);
}
}
Keys.onReleased: event => {
if (event.isAutoRepeat) {
event.accepted = true;
return;
}
if (useGridLayout) {
handleGridNavigation(event, false);
} else {
handleListNavigation(event, false);
}
}
Rectangle {
anchors.centerIn: parent
width: useGridLayout ? Math.min(550, gridColumns * 180 + Theme.spacingS * (gridColumns - 1) + Theme.spacingL * 2) : 320
height: contentItem.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Theme.outlineMedium
border.width: 1
Item {
id: contentItem
anchors.fill: parent
anchors.margins: Theme.spacingL
implicitHeight: headerRow.height + Theme.spacingM + (useGridLayout ? buttonGrid.implicitHeight : buttonColumn.implicitHeight) + (root.needsConfirmation ? hintRow.height + Theme.spacingM : 0)
Row {
id: headerRow
width: parent.width
height: 30
StyledText {
text: I18n.tr("Power Options")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - 150
height: 1
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: root.hide()
}
}
Grid {
id: buttonGrid
visible: useGridLayout
anchors.top: headerRow.bottom
anchors.topMargin: Theme.spacingM
anchors.horizontalCenter: parent.horizontalCenter
columns: root.gridColumns
columnSpacing: Theme.spacingS
rowSpacing: Theme.spacingS
width: parent.width
Repeater {
model: root.visibleActions
Rectangle {
id: gridButtonRect
required property int index
required property string modelData
readonly property var actionData: root.getActionData(modelData)
readonly property bool isSelected: root.selectedIndex === index
readonly property bool showWarning: modelData === "reboot" || modelData === "poweroff"
readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0
width: (contentItem.width - Theme.spacingS * (root.gridColumns - 1)) / root.gridColumns
height: 100
radius: Theme.cornerRadius
color: {
if (isSelected)
return Theme.primaryHover;
if (mouseArea.containsMouse)
return Theme.primaryHoverLight;
return Theme.surfaceHover;
}
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: isSelected ? 2 : 0
ClippingRectangle {
anchors.fill: parent
radius: parent.radius
color: "transparent"
visible: gridButtonRect.isHolding
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width * root.holdProgress
color: {
if (gridButtonRect.modelData === "poweroff")
return Theme.errorSelected;
if (gridButtonRect.modelData === "reboot")
return Theme.withAlpha(Theme.warning, 0.3);
return Theme.primarySelected;
}
}
}
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: gridButtonRect.actionData.icon
size: Theme.iconSize + 8
color: {
if (gridButtonRect.showWarning && (mouseArea.containsMouse || gridButtonRect.isHolding)) {
return gridButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning;
}
return Theme.surfaceText;
}
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: gridButtonRect.actionData.label
font.pixelSize: Theme.fontSizeMedium
color: {
if (gridButtonRect.showWarning && (mouseArea.containsMouse || gridButtonRect.isHolding)) {
return gridButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning;
}
return Theme.surfaceText;
}
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
Rectangle {
width: 20
height: 16
radius: 4
color: Theme.onSurface_12
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: gridButtonRect.actionData.key
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceTextSecondary
font.weight: Font.Medium
anchors.centerIn: parent
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
root.selectedRow = Math.floor(index / root.gridColumns);
root.selectedCol = index % root.gridColumns;
root.selectedIndex = index;
root.startHold(modelData, index);
}
onReleased: root.cancelHold()
onCanceled: root.cancelHold()
}
}
}
}
Column {
id: buttonColumn
visible: !useGridLayout
anchors.top: headerRow.bottom
anchors.topMargin: Theme.spacingM
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.spacingS
Repeater {
model: root.visibleActions
Rectangle {
id: listButtonRect
required property int index
required property string modelData
readonly property var actionData: root.getActionData(modelData)
readonly property bool isSelected: root.selectedIndex === index
readonly property bool showWarning: modelData === "reboot" || modelData === "poweroff"
readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
if (isSelected)
return Theme.primaryHover;
if (listMouseArea.containsMouse)
return Theme.primaryHoverLight;
return Theme.surfaceHover;
}
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: isSelected ? 2 : 0
ClippingRectangle {
anchors.fill: parent
radius: parent.radius
color: "transparent"
visible: listButtonRect.isHolding
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width * root.holdProgress
color: {
if (listButtonRect.modelData === "poweroff")
return Theme.errorSelected;
if (listButtonRect.modelData === "reboot")
return Theme.withAlpha(Theme.warning, 0.3);
return Theme.primarySelected;
}
}
}
Row {
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: listButtonRect.actionData.icon
size: Theme.iconSize + 4
color: {
if (listButtonRect.showWarning && (listMouseArea.containsMouse || listButtonRect.isHolding)) {
return listButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning;
}
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: listButtonRect.actionData.label
font.pixelSize: Theme.fontSizeMedium
color: {
if (listButtonRect.showWarning && (listMouseArea.containsMouse || listButtonRect.isHolding)) {
return listButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning;
}
return Theme.surfaceText;
}
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Rectangle {
width: 28
height: 20
radius: 4
color: Theme.onSurface_12
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: listButtonRect.actionData.key
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextSecondary
font.weight: Font.Medium
anchors.centerIn: parent
}
}
MouseArea {
id: listMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
root.selectedIndex = index;
root.startHold(modelData, index);
}
onReleased: root.cancelHold()
onCanceled: root.cancelHold()
}
}
}
}
Row {
id: hintRow
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Theme.spacingS
spacing: Theme.spacingXS
visible: root.needsConfirmation
opacity: root.showHoldHint ? 1 : 0.5
Behavior on opacity {
NumberAnimation {
duration: 150
}
}
DankIcon {
name: root.showHoldHint ? "warning" : "touch_app"
size: Theme.fontSizeSmall
color: root.showHoldHint ? Theme.warning : Theme.surfaceTextSecondary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
readonly property real totalMs: root.holdDurationMs
readonly property int remainingMs: Math.ceil(totalMs * (1 - root.holdProgress))
readonly property real durationSec: root.holdDurationMs / 1000
text: {
if (root.showHoldHint)
return I18n.tr("Hold longer to confirm");
if (root.holdProgress > 0) {
if (totalMs < 1000)
return I18n.tr("Hold to confirm (%1 ms)").arg(remainingMs);
return I18n.tr("Hold to confirm (%1s)").arg(Math.ceil(remainingMs / 1000));
}
if (totalMs < 1000)
return I18n.tr("Hold to confirm (%1 ms)").arg(totalMs);
return I18n.tr("Hold to confirm (%1s)").arg(durationSec);
}
font.pixelSize: Theme.fontSizeSmall
color: root.showHoldHint ? Theme.warning : Theme.surfaceTextSecondary
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
Component.onCompleted: updateVisibleActions()
}
@@ -12,7 +12,8 @@ import qs.Common
import qs.Modals import qs.Modals
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
import "../../Common/LayoutCodes.js" as LayoutCodes import qs.DankCommon.Session
import "../../DankCommon/Common/LayoutCodes.js" as LayoutCodes
Item { Item {
id: root id: root
+72 -40
View File
@@ -82,27 +82,44 @@ Item {
property bool greeterBinaryExists: false property bool greeterBinaryExists: false
property bool greeterEnabled: false property bool greeterEnabled: false
readonly property bool greeterInstalled: greeterBinaryExists || greeterEnabled readonly property bool greeterInstalled: greeterBinaryExists || greeterEnabled
readonly property string greeterAction: {
if (!greeterInstalled)
return "install";
if (!greeterEnabled)
return "activate";
return "";
}
readonly property bool greeterActionAvailable: greeterAction !== ""
readonly property string greeterActionLabel: { readonly property string greeterActionLabel: {
if (!root.greeterInstalled) switch (greeterAction) {
case "install":
return I18n.tr("Install"); return I18n.tr("Install");
if (!root.greeterEnabled) case "activate":
return I18n.tr("Activate"); return I18n.tr("Activate");
return I18n.tr("Uninstall"); default:
return "";
}
} }
readonly property string greeterActionIcon: { readonly property string greeterActionIcon: {
if (!root.greeterInstalled) switch (greeterAction) {
case "install":
return "download"; return "download";
if (!root.greeterEnabled) case "activate":
return "login"; return "login";
return "delete"; default:
return "";
}
} }
readonly property var greeterActionCommand: { readonly property var greeterActionCommand: {
if (!root.greeterInstalled) switch (greeterAction) {
case "install":
return ["dms", "greeter", "install", "--terminal"]; return ["dms", "greeter", "install", "--terminal"];
if (!root.greeterEnabled) case "activate":
return ["dms", "greeter", "enable", "--terminal"]; return ["dms", "greeter", "enable", "--terminal"];
return ["dms", "greeter", "uninstall", "--terminal", "--yes"]; default:
return [];
}
} }
property string greeterPendingAction: "" property string greeterPendingAction: ""
@@ -120,26 +137,28 @@ Item {
} }
function runGreeterInstallAction() { function runGreeterInstallAction() {
root.greeterPendingAction = !root.greeterInstalled ? "install" : !root.greeterEnabled ? "activate" : "uninstall"; root.greeterPendingAction = root.greeterAction;
greeterStatusText = I18n.tr("Opening terminal: ") + root.greeterActionLabel + "..."; greeterStatusText = I18n.tr("Opening terminal: ") + root.greeterActionLabel + "...";
greeterInstallActionRunning = true; greeterInstallActionRunning = true;
greeterInstallActionProcess.running = true; greeterInstallActionProcess.running = true;
} }
function promptGreeterActionConfirm() { function promptGreeterActionConfirm() {
if (!root.greeterActionAvailable)
return;
var title, message, confirmText; var title, message, confirmText;
if (!root.greeterInstalled) { switch (root.greeterAction) {
case "install":
title = I18n.tr("Install Greeter", "greeter action confirmation"); title = I18n.tr("Install Greeter", "greeter action confirmation");
message = I18n.tr("Install the DMS greeter? A terminal will open for sudo authentication."); message = I18n.tr("Install the DMS greeter? A terminal will open for sudo authentication.");
confirmText = I18n.tr("Install"); confirmText = I18n.tr("Install");
} else if (!root.greeterEnabled) { break;
case "activate":
title = I18n.tr("Activate Greeter", "greeter action confirmation"); title = I18n.tr("Activate Greeter", "greeter action confirmation");
message = I18n.tr("Activate the DMS greeter? A terminal will open for sudo authentication. Run Sync after activation to apply your settings."); message = I18n.tr("Activate the DMS greeter? A terminal will open for sudo authentication. Run Sync after activation to apply your settings.");
confirmText = I18n.tr("Activate"); confirmText = I18n.tr("Activate");
} else { break;
title = I18n.tr("Uninstall Greeter", "greeter action confirmation");
message = I18n.tr("Uninstall the DMS greeter? This will remove configuration and restore your previous display manager. A terminal will open for sudo authentication.");
confirmText = I18n.tr("Uninstall");
} }
greeterActionConfirm.showWithOptions({ greeterActionConfirm.showWithOptions({
"title": title, "title": title,
@@ -247,7 +266,17 @@ Item {
root.greeterSyncRunning = false; root.greeterSyncRunning = false;
const out = (root.greeterSyncStdout || "").trim(); const out = (root.greeterSyncStdout || "").trim();
const err = (root.greeterSyncStderr || "").trim(); const err = (root.greeterSyncStderr || "").trim();
if (exitCode === 0) { root.checkGreeterInstallState();
if (exitCode !== 0) {
var failure = I18n.tr("Sync failed in background mode. Trying terminal mode so you can authenticate interactively.") + " (exit " + exitCode + ")";
if (out !== "")
failure = failure + "\n\n" + out;
if (err !== "")
failure = failure + "\n\nstderr:\n" + err;
root.greeterStatusText = failure;
root.launchGreeterSyncTerminalFallback(false, "");
return;
}
var success = I18n.tr("Sync completed successfully."); var success = I18n.tr("Sync completed successfully.");
if (out !== "") if (out !== "")
success = success + "\n\n" + out; success = success + "\n\n" + out;
@@ -256,16 +285,6 @@ Item {
root.greeterStatusText = success; root.greeterStatusText = success;
SettingsData.clearGreeterSyncPending(); SettingsData.clearGreeterSyncPending();
ToastService.showInfo(I18n.tr("Greeter sync complete")); ToastService.showInfo(I18n.tr("Greeter sync complete"));
} else {
var failure = I18n.tr("Sync failed in background mode. Trying terminal mode so you can authenticate interactively.") + " (exit " + exitCode + ")";
if (out !== "")
failure = failure + "\n\n" + out;
if (err !== "")
failure = failure + "\n\nstderr:\n" + err;
root.greeterStatusText = failure;
root.launchGreeterSyncTerminalFallback(false, "");
}
root.checkGreeterInstallState();
} }
} }
@@ -327,17 +346,19 @@ Item {
root.greeterInstallActionRunning = false; root.greeterInstallActionRunning = false;
const pending = root.greeterPendingAction; const pending = root.greeterPendingAction;
root.greeterPendingAction = ""; root.greeterPendingAction = "";
if (exitCode === 0) {
if (pending === "install")
root.greeterStatusText = I18n.tr("Install complete. Greeter has been installed.");
else if (pending === "activate")
root.greeterStatusText = I18n.tr("Greeter activated. greetd is now enabled.");
else
root.greeterStatusText = I18n.tr("Uninstall complete. Greeter has been removed.");
} else {
root.greeterStatusText = I18n.tr("Action failed or terminal was closed.") + " (exit " + exitCode + ")";
}
root.checkGreeterInstallState(); root.checkGreeterInstallState();
if (exitCode !== 0) {
root.greeterStatusText = I18n.tr("Action failed or terminal was closed.") + " (exit " + exitCode + ")";
return;
}
switch (pending) {
case "install":
root.greeterStatusText = I18n.tr("Install complete. Greeter has been installed.");
return;
default:
root.greeterStatusText = I18n.tr("Greeter activated. greetd is now enabled.");
return;
}
} }
} }
@@ -423,7 +444,13 @@ Item {
id: statusTextArea id: statusTextArea
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingM anchors.margins: Theme.spacingM
text: root.greeterStatusRunning ? I18n.tr("Checking...", "greeter status loading") : (root.greeterStatusText || I18n.tr("Click Refresh to check status.", "greeter status placeholder")) text: {
if (root.greeterStatusRunning)
return I18n.tr("Checking...", "greeter status loading");
if (root.greeterStatusText !== "")
return root.greeterStatusText;
return I18n.tr("Click Refresh to check status.", "greeter status placeholder");
}
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.family: "monospace" font.family: "monospace"
color: root.greeterStatusRunning ? Theme.surfaceVariantText : Theme.surfaceText color: root.greeterStatusRunning ? Theme.surfaceVariantText : Theme.surfaceText
@@ -442,6 +469,7 @@ Item {
spacing: Theme.spacingS spacing: Theme.spacingS
DankButton { DankButton {
visible: root.greeterActionAvailable
text: root.greeterActionLabel text: root.greeterActionLabel
iconName: root.greeterActionIcon iconName: root.greeterActionIcon
horizontalPadding: Theme.spacingL horizontalPadding: Theme.spacingL
@@ -556,9 +584,13 @@ Item {
description: I18n.tr("Format the date on the login screen") description: I18n.tr("Format the date on the login screen")
options: root._lockDateFormatPresets.map(p => p.label) options: root._lockDateFormatPresets.map(p => p.label)
currentValue: { currentValue: {
var current = (SettingsData.greeterLockDateFormat !== undefined && SettingsData.greeterLockDateFormat !== "") ? SettingsData.greeterLockDateFormat : SettingsData.lockDateFormat || ""; var current = SettingsData.greeterLockDateFormat || SettingsData.lockDateFormat || "";
var match = root._lockDateFormatPresets.find(p => p.format === current); var match = root._lockDateFormatPresets.find(p => p.format === current);
return match ? match.label : (current ? I18n.tr("Custom") + ": " + current : root._lockDateFormatPresets[0].label); if (match)
return match.label;
if (current)
return I18n.tr("Custom") + ": " + current;
return root._lockDateFormatPresets[0].label;
} }
onValueChanged: value => { onValueChanged: value => {
var preset = root._lockDateFormatPresets.find(p => p.label === value); var preset = root._lockDateFormatPresets.find(p => p.label === value);
+1 -6
View File
@@ -13,12 +13,7 @@ Variants {
// An entry present in PanelWindow.onCompleted means we're recreating // An entry present in PanelWindow.onCompleted means we're recreating
// after a wl_output rebind, not at initial startup. // after a wl_output rebind, not at initial startup.
property var _seenScreens: ({}) property var _seenScreens: ({})
model: { model: SettingsData.getFilteredScreens("wallpaper")
if (SessionData.isGreeterMode) {
return Quickshell.screens;
}
return SettingsData.getFilteredScreens("wallpaper");
}
PanelWindow { PanelWindow {
id: wallpaperWindow id: wallpaperWindow
-1
View File
@@ -42,7 +42,6 @@ make lint-qml # Run from repo root; requires quickshell/.qmlls.ini (generated b
**System Controls** **System Controls**
- `Modules/ControlCenter/` - WiFi, Bluetooth, audio, display settings - `Modules/ControlCenter/` - WiFi, Bluetooth, audio, display settings
- `Modules/Notifications/` - Notification center with popups - `Modules/Notifications/` - Notification center with popups
- `Modules/Greetd/` - Login greeter interface
**Overlays** **Overlays**
- `Modules/Spotlight/` - Application and file launcher - `Modules/Spotlight/` - Application and file launcher
-163
View File
@@ -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);
}
}
-50
View File
@@ -53,10 +53,6 @@ Singleton {
profileImage = ""; profileImage = "";
return; return;
} }
if (Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true") {
profileImage = "";
return;
}
if (!freedeskAvailable) { if (!freedeskAvailable) {
profileImage = ""; profileImage = "";
@@ -299,52 +295,6 @@ Singleton {
}); });
} }
property string pendingGreeterProfileUser: ""
function getGreeterUserProfileImage(username) {
if (!username) {
profileImage = "";
pendingGreeterProfileUser = "";
return;
}
if (typeof GreeterUsersService !== "undefined") {
const cachedPath = GreeterUsersService.profileImagePath(username);
if (cachedPath) {
profileImage = cachedPath;
pendingGreeterProfileUser = "";
return;
}
}
pendingGreeterProfileUser = username;
userProfileCheckProcess.command = ["bash", "-c", `uid=$(id -u ${username} 2>/dev/null) && [ -n "$uid" ] && dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$uid org.freedesktop.DBus.Properties.Get string:org.freedesktop.Accounts.User string:IconFile 2>/dev/null | grep -oP 'string "\\K[^"]+' || echo ""`];
userProfileCheckProcess.running = true;
}
Process {
id: userProfileCheckProcess
command: []
running: false
stdout: StdioCollector {
onStreamFinished: {
const trimmed = text.trim();
if (trimmed && trimmed !== "" && !trimmed.includes("Error") && trimmed !== "/var/lib/AccountsService/icons/") {
root.profileImage = trimmed;
} else {
root.profileImage = "";
}
root.pendingGreeterProfileUser = "";
}
}
onExited: exitCode => {
if (exitCode !== 0 && root.pendingGreeterProfileUser !== "") {
root.profileImage = "";
root.pendingGreeterProfileUser = "";
}
}
}
Process { Process {
id: colorSchemeDetector id: colorSchemeDetector
command: ["bash", "-c", "command -v gsettings || command -v dconf"] command: ["bash", "-c", "command -v gsettings || command -v dconf"]

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