summaryrefslogtreecommitdiff
path: root/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd')
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreetdEnv.js20
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreetdMemory.qml128
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreetdSettings.qml199
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreeterContent.qml1908
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreeterState.qml30
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreeterSurface.qml34
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/README.md277
-rwxr-xr-xraveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/dms-greeter424
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/dms-hypr.conf3
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/dms-niri.kdl23
-rwxr-xr-xraveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/greet-hyprland.sh11
-rwxr-xr-xraveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/greet-niri.sh8
12 files changed, 3065 insertions, 0 deletions
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreetdEnv.js b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreetdEnv.js
new file mode 100644
index 0000000..fcce181
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreetdEnv.js
@@ -0,0 +1,20 @@
+.pragma library
+
+function readBoolOverride(envReader, names, fallbackValue) {
+ for (let i = 0; i < names.length; i++) {
+ const name = names[i];
+ const raw = envReader(name);
+ if (raw === undefined || raw === null || raw === "")
+ continue;
+
+ const normalized = String(raw).trim().toLowerCase();
+ if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on")
+ return true;
+ if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off")
+ return false;
+
+ console.warn("Invalid boolean override for", name + ":", raw, "- trying next override/fallback");
+ }
+
+ return fallbackValue;
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreetdMemory.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreetdMemory.qml
new file mode 100644
index 0000000..da949d3
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreetdMemory.qml
@@ -0,0 +1,128 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import "GreetdEnv.js" as GreetdEnv
+
+Singleton {
+ id: root
+
+ 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 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) {
+ console.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 || "") : "";
+ lastSuccessfulUser = rememberLastUser ? (memory.lastSuccessfulUser || "") : "";
+ if (!rememberLastSession || !rememberLastUser)
+ saveMemory();
+ } catch (e) {
+ console.warn("Failed to parse greetd memory:", e);
+ }
+ }
+
+ function saveMemory() {
+ let memory = {};
+ if (rememberLastSession && lastSessionId)
+ memory.lastSessionId = lastSessionId;
+ if (rememberLastUser && lastSuccessfulUser)
+ memory.lastSuccessfulUser = lastSuccessfulUser;
+ memoryFileView.setText(JSON.stringify(memory, null, 2));
+ }
+
+ function setLastSessionId(id) {
+ if (!rememberLastSession) {
+ if (lastSessionId !== "") {
+ lastSessionId = "";
+ saveMemory();
+ }
+ return;
+ }
+ lastSessionId = id || "";
+ 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: true
+ onLoaded: {
+ parseSessionConfig(sessionConfigFileView.text());
+ }
+ onLoadFailed: error => {
+ console.warn("Could not load greeter session config from", root.sessionConfigPath, "error:", error);
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreetdSettings.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreetdSettings.qml
new file mode 100644
index 0000000..e1f670c
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreetdSettings.qml
@@ -0,0 +1,199 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import qs.Common
+import "GreetdEnv.js" as GreetdEnv
+
+Singleton {
+ id: root
+
+ readonly property string configPath: {
+ const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
+ return greetCfgDir + "/settings.json";
+ }
+
+ readonly property string _greeterCacheDir: {
+ const i = root.configPath.lastIndexOf("/");
+ return i >= 0 ? root.configPath.substring(0, i) : "";
+ }
+ readonly property string greeterWallpaperOverridePath: root._greeterCacheDir ? (root._greeterCacheDir + "/greeter_wallpaper_override.jpg") : ""
+
+ property string currentThemeName: "purple"
+ property bool settingsLoaded: false
+ property string customThemeFile: ""
+ property var registryThemeVariants: ({})
+ property string matugenScheme: "scheme-tonal-spot"
+ property bool use24HourClock: true
+ property bool showSeconds: false
+ property bool padHours12Hour: false
+ property bool greeterUse24HourClock: true
+ property bool greeterShowSeconds: false
+ property bool greeterPadHours12Hour: 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 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"
+
+ 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";
+ use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
+ showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
+ padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : false;
+ greeterUse24HourClock = settings.greeterUse24HourClock !== undefined ? settings.greeterUse24HourClock : use24HourClock;
+ greeterShowSeconds = settings.greeterShowSeconds !== undefined ? settings.greeterShowSeconds : showSeconds;
+ greeterPadHours12Hour = settings.greeterPadHours12Hour !== undefined ? settings.greeterPadHours12Hour : padHours12Hour;
+ 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;
+ }
+ 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";
+
+ if (typeof Theme !== "undefined") {
+ if (currentThemeName === "custom" && customThemeFile) {
+ Theme.loadCustomThemeFromFile(customThemeFile);
+ }
+ Theme.applyGreeterTheme(currentThemeName);
+ }
+ } catch (e) {
+ console.warn("Failed to parse greetd settings:", e);
+ } finally {
+ settingsLoaded = true;
+ }
+ }
+
+ function getEffectiveTimeFormat() {
+ const use24 = greeterUse24HourClock;
+ const secs = greeterShowSeconds;
+ const pad = greeterPadHours12Hour;
+ 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: true
+ onLoaded: {
+ parseSettings(settingsFile.text());
+ }
+ onLoadFailed: error => {
+ console.warn("Failed to load greetd settings:", error);
+ root.parseSettings("");
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreeterContent.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreeterContent.qml
new file mode 100644
index 0000000..fa82315
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreeterContent.qml
@@ -0,0 +1,1908 @@
+import QtCore
+import QtQuick
+import QtQuick.Effects
+import QtQuick.Layouts
+import Qt.labs.folderlistmodel
+import Quickshell
+import Quickshell.Hyprland
+import Quickshell.Io
+import Quickshell.Services.Greetd
+import qs.Common
+import qs.Services
+import qs.Widgets
+import qs.Modules.Lock
+
+Item {
+ id: root
+
+ function encodeFileUrl(path) {
+ if (!path)
+ return "";
+ return "file://" + path.split('/').map(s => encodeURIComponent(s)).join('/');
+ }
+
+ readonly property string xdgDataDirs: Quickshell.env("XDG_DATA_DIRS")
+ property string screenName: ""
+ property string hyprlandCurrentLayout: ""
+ property string hyprlandKeyboard: ""
+ property int hyprlandLayoutCount: 0
+ property bool isPrimaryScreen: !Quickshell.screens?.length || screenName === Quickshell.screens[0]?.name
+
+ signal launchRequested
+
+ property bool weatherInitialized: false
+ property bool awaitingExternalAuth: false
+ property bool pendingPasswordResponse: false
+ property bool passwordSubmitRequested: false
+ property bool cancelingExternalAuthForPassword: false
+ property int defaultAuthTimeoutMs: 10000
+ property int externalAuthTimeoutMs: 30000
+ property int memoryFlushDelayMs: 120
+ property string pendingLaunchCommand: ""
+ property var pendingLaunchEnv: []
+ property int passwordFailureCount: 0
+ property int passwordAttemptLimitHint: 0
+ property string authFeedbackMessage: ""
+ property string greetdPamText: ""
+ property string systemAuthPamText: ""
+ property string commonAuthPamText: ""
+ property string passwordAuthPamText: ""
+ property string systemLoginPamText: ""
+ property string systemLocalLoginPamText: ""
+ property string commonAuthPcPamText: ""
+ property string loginPamText: ""
+ property string faillockConfigText: ""
+ property bool greeterWallpaperOverrideExists: false
+ property string externalAuthAutoStartedForUser: ""
+ property int passwordSessionTransitionRetryCount: 0
+ property int maxPasswordSessionTransitionRetries: 2
+ property bool fprintdProbeComplete: false
+ property bool fprintdHasDevice: false
+ // Falls back to PAM-only detection until the fprintd D-Bus probe completes.
+ readonly property bool greeterPamHasFprint: greeterPamStackHasModule("pam_fprintd") && (!fprintdProbeComplete || fprintdHasDevice)
+ readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f")
+ readonly property bool greeterExternalAuthAvailable: (greeterPamHasFprint && GreetdSettings.greeterEnableFprint) || (greeterPamHasU2f && GreetdSettings.greeterEnableU2f)
+ readonly property bool greeterPamHasExternalAuth: greeterPamHasFprint || greeterPamHasU2f
+
+ function initWeatherService() {
+ if (weatherInitialized)
+ return;
+ if (!GreetdSettings.settingsLoaded)
+ return;
+ if (!GreetdSettings.weatherEnabled)
+ return;
+ weatherInitialized = true;
+ WeatherService.addRef();
+ WeatherService.forceRefresh();
+ }
+
+ function stripPamComment(line) {
+ if (!line)
+ return "";
+ const trimmed = line.trim();
+ if (!trimmed || trimmed.startsWith("//"))
+ return "";
+ const hashIdx = trimmed.indexOf("//");
+ if (hashIdx >= 0)
+ return trimmed.substring(0, hashIdx).trim();
+ return trimmed;
+ }
+
+ function pamModuleEnabled(pamText, moduleName) {
+ if (!pamText || !moduleName)
+ return false;
+ const lines = pamText.split(/\r?\n/);
+ for (let i = 0; i < lines.length; i++) {
+ const line = stripPamComment(lines[i]);
+ if (!line)
+ continue;
+ if (line.includes(moduleName))
+ return true;
+ }
+ return false;
+ }
+
+ function pamTextIncludesFile(pamText, filename) {
+ if (!pamText || !filename)
+ return false;
+ const lines = pamText.split(/\r?\n/);
+ for (let i = 0; i < lines.length; i++) {
+ const line = stripPamComment(lines[i]);
+ if (!line)
+ continue;
+ if (line.includes(filename) && (line.includes("include") || line.includes("substack") || line.startsWith("@include")))
+ return true;
+ }
+ return false;
+ }
+
+ function greeterPamStackHasModule(moduleName) {
+ if (pamModuleEnabled(greetdPamText, moduleName))
+ return true;
+ const includedPamStacks = [
+ ["system-auth", systemAuthPamText],
+ ["common-auth", commonAuthPamText],
+ ["password-auth", passwordAuthPamText],
+ ["system-login", systemLoginPamText],
+ ["system-local-login", systemLocalLoginPamText],
+ ["common-auth-pc", commonAuthPcPamText],
+ ["login", loginPamText]
+ ];
+ for (let i = 0; i < includedPamStacks.length; i++) {
+ const stack = includedPamStacks[i];
+ if (pamTextIncludesFile(greetdPamText, stack[0]) && pamModuleEnabled(stack[1], moduleName))
+ return true;
+ }
+ return false;
+ }
+
+ function usesPamLockoutPolicy(pamText) {
+ if (!pamText)
+ return false;
+ const lines = pamText.split(/\r?\n/);
+ for (let i = 0; i < lines.length; i++) {
+ const line = stripPamComment(lines[i]);
+ if (!line)
+ continue;
+ if (line.includes("pam_faillock.so") || line.includes("pam_tally2.so") || line.includes("pam_tally.so"))
+ return true;
+ }
+ return false;
+ }
+
+ function parsePamLineDenyValue(pamText) {
+ if (!pamText)
+ return -1;
+ const lines = pamText.split(/\r?\n/);
+ for (let i = 0; i < lines.length; i++) {
+ const line = stripPamComment(lines[i]);
+ if (!line)
+ continue;
+ if (!line.includes("pam_faillock.so") && !line.includes("pam_tally2.so") && !line.includes("pam_tally.so"))
+ continue;
+ const denyMatch = line.match(/\bdeny\s*=\s*(\d+)\b/i);
+ if (!denyMatch)
+ continue;
+ const parsed = parseInt(denyMatch[1], 10);
+ if (!isNaN(parsed))
+ return parsed;
+ }
+ return -1;
+ }
+
+ function parseFaillockDenyValue(configText) {
+ if (!configText)
+ return -1;
+ const lines = configText.split(/\r?\n/);
+ for (let i = 0; i < lines.length; i++) {
+ const line = stripPamComment(lines[i]);
+ if (!line)
+ continue;
+ const denyMatch = line.match(/^deny\s*=\s*(\d+)\s*$/i);
+ if (!denyMatch)
+ continue;
+ const parsed = parseInt(denyMatch[1], 10);
+ if (!isNaN(parsed))
+ return parsed;
+ }
+ return -1;
+ }
+
+ function refreshPasswordAttemptPolicyHint() {
+ const pamSources = [greetdPamText, systemAuthPamText, commonAuthPamText, passwordAuthPamText, systemLoginPamText, systemLocalLoginPamText, commonAuthPcPamText, loginPamText];
+ let lockoutConfigured = false;
+ let denyFromPam = -1;
+ for (let i = 0; i < pamSources.length; i++) {
+ const source = pamSources[i];
+ if (!source)
+ continue;
+ if (usesPamLockoutPolicy(source))
+ lockoutConfigured = true;
+ const denyValue = parsePamLineDenyValue(source);
+ if (denyValue >= 0 && (denyFromPam < 0 || denyValue < denyFromPam))
+ denyFromPam = denyValue;
+ }
+
+ if (!lockoutConfigured) {
+ passwordAttemptLimitHint = 0;
+ return;
+ }
+
+ const denyFromConfig = parseFaillockDenyValue(faillockConfigText);
+ if (denyFromConfig >= 0) {
+ passwordAttemptLimitHint = denyFromConfig;
+ return;
+ }
+
+ if (denyFromPam >= 0) {
+ passwordAttemptLimitHint = denyFromPam;
+ return;
+ }
+
+ // pam_faillock default deny value when no explicit config is set.
+ passwordAttemptLimitHint = 3;
+ }
+
+ function isLikelyLockoutMessage(message) {
+ const lower = (message || "").toLowerCase();
+ return lower.includes("account is locked") || lower.includes("too many") || lower.includes("maximum number of") || lower.includes("auth_err");
+ }
+
+ function currentAuthMessage() {
+ if (GreeterState.pamState === "error")
+ return I18n.tr("Authentication error - try again");
+ if (GreeterState.pamState === "max")
+ return I18n.tr("Too many failed attempts - account may be locked");
+ if (GreeterState.pamState === "fail") {
+ if (passwordAttemptLimitHint > 0) {
+ const attempt = Math.max(1, Math.min(passwordFailureCount, passwordAttemptLimitHint));
+ const remaining = Math.max(passwordAttemptLimitHint - attempt, 0);
+ if (remaining > 0) {
+ return I18n.tr("Incorrect password - attempt %1 of %2 (lockout may follow)").arg(attempt).arg(passwordAttemptLimitHint);
+ }
+ return I18n.tr("Incorrect password - next failures may trigger account lockout");
+ }
+ return I18n.tr("Incorrect password");
+ }
+ return "";
+ }
+
+ function clearAuthFeedback() {
+ GreeterState.pamState = "";
+ authFeedbackMessage = "";
+ }
+
+ function resetPasswordSessionTransition(clearSubmitRequest) {
+ cancelingExternalAuthForPassword = false;
+ passwordSessionTransitionRetryCount = 0;
+ if (clearSubmitRequest)
+ passwordSubmitRequested = false;
+ }
+
+ Connections {
+ target: GreetdSettings
+ function onSettingsLoadedChanged() {
+ if (GreetdSettings.settingsLoaded) {
+ initWeatherService();
+ if (isPrimaryScreen) {
+ applyLastSuccessfulUser();
+ finalizeSessionSelection();
+ }
+ }
+ }
+
+ function onRememberLastUserChanged() {
+ if (!isPrimaryScreen)
+ return;
+ if (!GreetdSettings.rememberLastUser && GreetdMemory.lastSuccessfulUser) {
+ GreetdMemory.setLastSuccessfulUser("");
+ }
+ applyLastSuccessfulUser();
+ }
+
+ function onRememberLastSessionChanged() {
+ if (!isPrimaryScreen)
+ return;
+ if (!GreetdSettings.rememberLastSession && GreetdMemory.lastSessionId) {
+ GreetdMemory.setLastSessionId("");
+ }
+ finalizeSessionSelection();
+ }
+ }
+
+ FileView {
+ id: greetdPamWatcher
+ path: "/etc/pam.d/greetd"
+ printErrors: false
+ onLoaded: {
+ root.greetdPamText = text();
+ root.refreshPasswordAttemptPolicyHint();
+ root.maybeAutoStartExternalAuth();
+ }
+ onLoadFailed: {
+ root.greetdPamText = "";
+ root.refreshPasswordAttemptPolicyHint();
+ }
+ }
+
+ FileView {
+ id: systemAuthPamWatcher
+ path: "/etc/pam.d/system-auth"
+ printErrors: false
+ onLoaded: {
+ root.systemAuthPamText = text();
+ root.refreshPasswordAttemptPolicyHint();
+ root.maybeAutoStartExternalAuth();
+ }
+ onLoadFailed: {
+ root.systemAuthPamText = "";
+ root.refreshPasswordAttemptPolicyHint();
+ }
+ }
+
+ FileView {
+ id: commonAuthPamWatcher
+ path: "/etc/pam.d/common-auth"
+ printErrors: false
+ onLoaded: {
+ root.commonAuthPamText = text();
+ root.refreshPasswordAttemptPolicyHint();
+ root.maybeAutoStartExternalAuth();
+ }
+ onLoadFailed: {
+ root.commonAuthPamText = "";
+ root.refreshPasswordAttemptPolicyHint();
+ }
+ }
+
+ FileView {
+ id: passwordAuthPamWatcher
+ path: "/etc/pam.d/password-auth"
+ printErrors: false
+ onLoaded: {
+ root.passwordAuthPamText = text();
+ root.refreshPasswordAttemptPolicyHint();
+ root.maybeAutoStartExternalAuth();
+ }
+ onLoadFailed: {
+ root.passwordAuthPamText = "";
+ root.refreshPasswordAttemptPolicyHint();
+ }
+ }
+
+ FileView {
+ id: systemLoginPamWatcher
+ path: "/etc/pam.d/system-login"
+ printErrors: false
+ onLoaded: {
+ root.systemLoginPamText = text();
+ root.refreshPasswordAttemptPolicyHint();
+ root.maybeAutoStartExternalAuth();
+ }
+ onLoadFailed: {
+ root.systemLoginPamText = "";
+ root.refreshPasswordAttemptPolicyHint();
+ }
+ }
+
+ FileView {
+ id: systemLocalLoginPamWatcher
+ path: "/etc/pam.d/system-local-login"
+ printErrors: false
+ onLoaded: {
+ root.systemLocalLoginPamText = text();
+ root.refreshPasswordAttemptPolicyHint();
+ root.maybeAutoStartExternalAuth();
+ }
+ onLoadFailed: {
+ root.systemLocalLoginPamText = "";
+ root.refreshPasswordAttemptPolicyHint();
+ }
+ }
+
+ FileView {
+ id: commonAuthPcPamWatcher
+ path: "/etc/pam.d/common-auth-pc"
+ printErrors: false
+ onLoaded: {
+ root.commonAuthPcPamText = text();
+ root.refreshPasswordAttemptPolicyHint();
+ root.maybeAutoStartExternalAuth();
+ }
+ onLoadFailed: {
+ root.commonAuthPcPamText = "";
+ root.refreshPasswordAttemptPolicyHint();
+ }
+ }
+
+ FileView {
+ id: loginPamWatcher
+ path: "/etc/pam.d/login"
+ printErrors: false
+ onLoaded: {
+ root.loginPamText = text();
+ root.refreshPasswordAttemptPolicyHint();
+ root.maybeAutoStartExternalAuth();
+ }
+ onLoadFailed: {
+ root.loginPamText = "";
+ root.refreshPasswordAttemptPolicyHint();
+ }
+ }
+
+ FileView {
+ id: faillockConfigWatcher
+ path: "/etc/security/faillock.conf"
+ printErrors: false
+ onLoaded: {
+ root.faillockConfigText = text();
+ root.refreshPasswordAttemptPolicyHint();
+ }
+ onLoadFailed: {
+ root.faillockConfigText = "";
+ root.refreshPasswordAttemptPolicyHint();
+ }
+ }
+
+ Component.onCompleted: {
+ initWeatherService();
+ refreshPasswordAttemptPolicyHint();
+
+ if (isPrimaryScreen)
+ applyLastSuccessfulUser();
+
+ if (CompositorService.isHyprland)
+ updateHyprlandLayout();
+
+ fprintdDeviceProbe.running = true;
+ }
+
+ function applyLastSuccessfulUser() {
+ if (!GreetdSettings.settingsLoaded || !GreetdSettings.rememberLastUser)
+ return;
+ const lastUser = GreetdMemory.lastSuccessfulUser;
+ if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) {
+ GreeterState.username = lastUser;
+ GreeterState.usernameInput = lastUser;
+ GreeterState.showPasswordInput = true;
+ PortalService.getGreeterUserProfileImage(lastUser);
+ maybeAutoStartExternalAuth();
+ }
+ }
+
+ function submitUsername(rawValue) {
+ const user = (rawValue || "").trim();
+ if (!user)
+ return;
+ if (GreeterState.username !== user) {
+ passwordFailureCount = 0;
+ clearAuthFeedback();
+ externalAuthAutoStartedForUser = "";
+ }
+ GreeterState.username = user;
+ GreeterState.showPasswordInput = true;
+ PortalService.getGreeterUserProfileImage(user);
+ GreeterState.passwordBuffer = "";
+ pendingPasswordResponse = false;
+ resetPasswordSessionTransition(true);
+ maybeAutoStartExternalAuth();
+ }
+
+ function submitBufferedPassword() {
+ pendingPasswordResponse = false;
+ resetPasswordSessionTransition(true);
+ awaitingExternalAuth = false;
+ authTimeout.interval = defaultAuthTimeoutMs;
+ authTimeout.restart();
+ // Some PAM stacks expect an explicit empty response to advance U2F/fprint or fail normally.
+ Greetd.respond(GreeterState.passwordBuffer || "");
+ GreeterState.passwordBuffer = "";
+ inputField.text = "";
+ return true;
+ }
+
+ function requestPasswordSessionTransition() {
+ const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
+ if (!passwordSubmitRequested && !hasPasswordBuffer)
+ return;
+ if (cancelingExternalAuthForPassword)
+ return;
+ if (passwordSessionTransitionRetryCount >= maxPasswordSessionTransitionRetries) {
+ pendingPasswordResponse = false;
+ awaitingExternalAuth = false;
+ authTimeout.interval = defaultAuthTimeoutMs;
+ authTimeout.stop();
+ resetPasswordSessionTransition(true);
+ GreeterState.pamState = "error";
+ authFeedbackMessage = currentAuthMessage();
+ placeholderDelay.restart();
+ Greetd.cancelSession();
+ return;
+ }
+ cancelingExternalAuthForPassword = true;
+ passwordSessionTransitionRetryCount = passwordSessionTransitionRetryCount + 1;
+ awaitingExternalAuth = false;
+ pendingPasswordResponse = false;
+ authTimeout.interval = defaultAuthTimeoutMs;
+ authTimeout.stop();
+ Greetd.cancelSession();
+ }
+
+ function startAuthSession(submitPassword) {
+ submitPassword = submitPassword === true;
+ if (!GreeterState.showPasswordInput || !GreeterState.username)
+ return;
+ if (GreeterState.unlocking)
+ return;
+ const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
+ if (Greetd.state !== GreetdState.Inactive) {
+ if (pendingPasswordResponse && submitPassword)
+ submitBufferedPassword();
+ else if (submitPassword)
+ passwordSubmitRequested = true;
+ return;
+ }
+ if (cancelingExternalAuthForPassword) {
+ if (submitPassword)
+ passwordSubmitRequested = true;
+ return;
+ }
+ if (!submitPassword && !hasPasswordBuffer && !root.greeterExternalAuthAvailable)
+ return;
+ pendingPasswordResponse = false;
+ passwordSubmitRequested = submitPassword;
+ awaitingExternalAuth = !submitPassword && !hasPasswordBuffer && root.greeterExternalAuthAvailable;
+ // Use greeterExternalAuthAvailable so systems with pam_fprintd but no hardware don't incur the 30 s wait.
+ const waitingOnPamExternalBeforePassword = submitPassword && root.greeterExternalAuthAvailable;
+ authTimeout.interval = (awaitingExternalAuth || waitingOnPamExternalBeforePassword) ? externalAuthTimeoutMs : defaultAuthTimeoutMs;
+ authTimeout.restart();
+ Greetd.createSession(GreeterState.username);
+ }
+
+ function maybeAutoStartExternalAuth() {
+ if (!GreeterState.showPasswordInput || !GreeterState.username)
+ return;
+ if (!root.greeterExternalAuthAvailable)
+ return;
+ if (GreeterState.unlocking || Greetd.state !== GreetdState.Inactive)
+ return;
+ if (passwordSubmitRequested || cancelingExternalAuthForPassword)
+ return;
+ if (GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0)
+ return;
+ if (externalAuthAutoStartedForUser === GreeterState.username)
+ return;
+
+ externalAuthAutoStartedForUser = GreeterState.username;
+ startAuthSession(false);
+ }
+
+ function isExternalAuthPrompt(message, responseRequired) {
+ // Non-response PAM messages commonly represent waiting states (fprint/U2F/token touch).
+ return !responseRequired;
+ }
+
+ Component.onDestruction: {
+ if (weatherInitialized)
+ WeatherService.removeRef();
+ }
+
+ function updateHyprlandLayout() {
+ if (CompositorService.isHyprland) {
+ hyprlandLayoutProcess.running = true;
+ }
+ }
+
+ Process {
+ id: hyprlandLayoutProcess
+ running: false
+ command: ["hyprctl", "-j", "devices"]
+ stdout: StdioCollector {
+ onStreamFinished: {
+ try {
+ const data = JSON.parse(text);
+ const mainKeyboard = data.keyboards.find(kb => kb.main === true);
+ if (!mainKeyboard) {
+ hyprlandCurrentLayout = "";
+ hyprlandLayoutCount = 0;
+ return;
+ }
+ hyprlandKeyboard = mainKeyboard.name;
+ if (mainKeyboard.active_keymap) {
+ const parts = mainKeyboard.active_keymap.split(" ");
+ hyprlandCurrentLayout = parts[0].substring(0, 2).toUpperCase();
+ } else {
+ hyprlandCurrentLayout = "";
+ }
+ hyprlandLayoutCount = mainKeyboard.layout ? mainKeyboard.layout.split(",").length : 0;
+ } catch (e) {
+ hyprlandCurrentLayout = "";
+ hyprlandLayoutCount = 0;
+ }
+ }
+ }
+ }
+
+ // Probe fprintd D-Bus for physically enrolled scanners to eliminate PAM stack false-positives.
+ Process {
+ id: fprintdDeviceProbe
+ running: false
+ // sh wrapper: emits PROBE_UNAVAILABLE if gdbus is absent or fprintd unreachable,
+ // keeping the PAM-only fallback active in those cases.
+ command: ["sh", "-c",
+ "command -v gdbus >/dev/null 2>&1 || { echo PROBE_UNAVAILABLE; exit 0; }; " +
+ "gdbus call --system " +
+ "--dest net.reactivated.Fprint " +
+ "--object-path /net/reactivated/Fprint/Manager " +
+ "--method net.reactivated.Fprint.Manager.GetDevices 2>/dev/null " +
+ "|| echo PROBE_UNAVAILABLE"]
+ stdout: StdioCollector {
+ onStreamFinished: {
+ if (text.includes("PROBE_UNAVAILABLE"))
+ return; // PAM-only fallback stays active
+ root.fprintdHasDevice = text.includes("objectpath");
+ root.fprintdProbeComplete = true;
+ root.maybeAutoStartExternalAuth();
+ }
+ }
+ onExited: function(exitCode, exitStatus) {
+ if (!root.fprintdProbeComplete)
+ root.maybeAutoStartExternalAuth(); // PAM-only fallback stays active
+ }
+ }
+
+ Connections {
+ target: CompositorService.isHyprland ? Hyprland : null
+ enabled: CompositorService.isHyprland
+
+ function onRawEvent(event) {
+ if (event.name === "activelayout")
+ updateHyprlandLayout();
+ }
+ }
+
+ Connections {
+ target: GreetdMemory
+ enabled: isPrimaryScreen
+ function onLastSuccessfulUserChanged() {
+ applyLastSuccessfulUser();
+ }
+ function onMemoryReadyChanged() {
+ finalizeSessionSelection();
+ }
+ }
+
+ Connections {
+ target: GreeterState
+ function onUsernameChanged() {
+ if (GreeterState.username) {
+ PortalService.getGreeterUserProfileImage(GreeterState.username);
+ }
+ }
+ }
+
+ FileView {
+ id: greeterWallpaperOverrideFile
+ path: GreetdSettings.greeterWallpaperOverridePath
+ printErrors: false
+ watchChanges: true
+ onLoaded: root.greeterWallpaperOverrideExists = true
+ onLoadFailed: root.greeterWallpaperOverrideExists = false
+ }
+
+ Connections {
+ target: GreetdSettings
+ function onGreeterWallpaperOverridePathChanged() {
+ if (!GreetdSettings.greeterWallpaperOverridePath) {
+ root.greeterWallpaperOverrideExists = false;
+ return;
+ }
+ greeterWallpaperOverrideFile.reload();
+ }
+ function onGreeterWallpaperPathChanged() {
+ if (!GreetdSettings.greeterWallpaperPath) {
+ root.greeterWallpaperOverrideExists = false;
+ return;
+ }
+ greeterWallpaperOverrideFile.reload();
+ }
+ }
+
+ DankBackdrop {
+ anchors.fill: parent
+ screenName: root.screenName
+ visible: {
+ if (GreetdSettings.greeterWallpaperPath !== "" && root.greeterWallpaperOverrideExists)
+ return false;
+ var _ = SessionData.perMonitorWallpaper;
+ var __ = SessionData.monitorWallpapers;
+ var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
+ return !currentWallpaper || currentWallpaper === "" || (currentWallpaper && currentWallpaper.startsWith("#"));
+ }
+ }
+
+ Image {
+ id: wallpaperBackground
+
+ anchors.fill: parent
+ source: {
+ if (GreetdSettings.greeterWallpaperPath !== "" && root.greeterWallpaperOverrideExists)
+ return encodeFileUrl(GreetdSettings.greeterWallpaperOverridePath);
+ var _ = SessionData.perMonitorWallpaper;
+ var __ = SessionData.monitorWallpapers;
+ var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
+ return (currentWallpaper && !currentWallpaper.startsWith("#")) ? encodeFileUrl(currentWallpaper) : "";
+ }
+ fillMode: Theme.getFillMode(GreetdSettings.getEffectiveWallpaperFillMode())
+ smooth: true
+ asynchronous: false
+ cache: true
+ visible: source !== ""
+ layer.enabled: true
+
+ layer.effect: MultiEffect {
+ autoPaddingEnabled: false
+ blurEnabled: true
+ blur: 0.8
+ blurMax: 32
+ blurMultiplier: 1
+ }
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: "black"
+ opacity: 0.4
+ }
+
+ SystemClock {
+ id: systemClock
+ precision: SystemClock.Seconds
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: "transparent"
+
+ Item {
+ id: clockContainer
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottom: parent.verticalCenter
+ anchors.bottomMargin: 60
+ width: parent.width
+ height: clockText.implicitHeight
+
+ Row {
+ id: clockText
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ spacing: 0
+
+ property string fullTimeStr: {
+ const format = GreetdSettings.getEffectiveTimeFormat();
+ return systemClock.date.toLocaleTimeString(I18n.locale(), format);
+ }
+ property var timeParts: fullTimeStr.split(':')
+ property string hours: timeParts[0] || ""
+ property string minutes: timeParts[1] || ""
+ property string secondsWithAmPm: timeParts.length > 2 ? timeParts[2] : ""
+ property string seconds: secondsWithAmPm.replace(/\s*(AM|PM|am|pm)$/i, '')
+ property string ampm: {
+ const match = fullTimeStr.match(/\s*(AM|PM|am|pm)$/i);
+ return match ? match[0].trim() : "";
+ }
+ property bool hasSeconds: timeParts.length > 2
+
+ StyledText {
+ width: 75
+ text: clockText.hours.length > 1 ? clockText.hours[0] : ""
+ font.pixelSize: 120
+ font.weight: Font.Light
+ color: "white"
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ StyledText {
+ width: 75
+ text: clockText.hours.length > 1 ? clockText.hours[1] : clockText.hours.length > 0 ? clockText.hours[0] : ""
+ font.pixelSize: 120
+ font.weight: Font.Light
+ color: "white"
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ StyledText {
+ text: ":"
+ font.pixelSize: 120
+ font.weight: Font.Light
+ color: "white"
+ }
+
+ StyledText {
+ width: 75
+ text: clockText.minutes.length > 0 ? clockText.minutes[0] : ""
+ font.pixelSize: 120
+ font.weight: Font.Light
+ color: "white"
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ StyledText {
+ width: 75
+ text: clockText.minutes.length > 1 ? clockText.minutes[1] : ""
+ font.pixelSize: 120
+ font.weight: Font.Light
+ color: "white"
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ StyledText {
+ text: clockText.hasSeconds ? ":" : ""
+ font.pixelSize: 120
+ font.weight: Font.Light
+ color: "white"
+ visible: clockText.hasSeconds
+ }
+
+ StyledText {
+ width: 75
+ text: clockText.hasSeconds && clockText.seconds.length > 0 ? clockText.seconds[0] : ""
+ font.pixelSize: 120
+ font.weight: Font.Light
+ color: "white"
+ horizontalAlignment: Text.AlignHCenter
+ visible: clockText.hasSeconds
+ }
+
+ StyledText {
+ width: 75
+ text: clockText.hasSeconds && clockText.seconds.length > 1 ? clockText.seconds[1] : ""
+ font.pixelSize: 120
+ font.weight: Font.Light
+ color: "white"
+ horizontalAlignment: Text.AlignHCenter
+ visible: clockText.hasSeconds
+ }
+
+ StyledText {
+ width: 20
+ text: " "
+ font.pixelSize: 120
+ font.weight: Font.Light
+ color: "white"
+ visible: clockText.ampm !== ""
+ }
+
+ StyledText {
+ text: clockText.ampm
+ font.pixelSize: 120
+ font.weight: Font.Light
+ color: "white"
+ visible: clockText.ampm !== ""
+ }
+ }
+ }
+
+ StyledText {
+ id: dateText
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: clockContainer.bottom
+ anchors.topMargin: 4
+ text: {
+ return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.getEffectiveLockDateFormat());
+ }
+ font.pixelSize: Theme.fontSizeXLarge
+ color: "white"
+ opacity: 0.9
+ }
+
+ Item {
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: dateText.bottom
+ anchors.topMargin: Theme.spacingL
+ width: 380
+ height: 140
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: Theme.spacingM
+
+ RowLayout {
+ spacing: Theme.spacingL
+ Layout.fillWidth: true
+
+ DankCircularImage {
+ Layout.preferredWidth: 60
+ Layout.preferredHeight: 60
+ imageSource: {
+ if (PortalService.profileImage === "")
+ return "";
+ if (PortalService.profileImage.startsWith("/"))
+ return encodeFileUrl(PortalService.profileImage);
+ return PortalService.profileImage;
+ }
+ fallbackIcon: "person"
+ visible: GreetdSettings.lockScreenShowProfileImage
+ }
+
+ Rectangle {
+ property bool showPassword: false
+
+ Layout.fillWidth: true
+ Layout.preferredHeight: 60
+ radius: Theme.cornerRadius
+ color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.9)
+ border.color: inputField.activeFocus ? Theme.primary : Qt.rgba(1, 1, 1, 0.3)
+ border.width: inputField.activeFocus ? 2 : 1
+
+ DankIcon {
+ id: lockIcon
+
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ name: GreeterState.showPasswordInput ? "lock" : "person"
+ size: 20
+ color: inputField.activeFocus ? Theme.primary : Theme.surfaceVariantText
+ }
+
+ TextInput {
+ id: inputField
+
+ property bool syncingFromState: false
+
+ anchors.fill: parent
+ anchors.leftMargin: lockIcon.width + Theme.spacingM * 2
+ anchors.rightMargin: {
+ let margin = Theme.spacingM;
+ if (GreeterState.showPasswordInput && revealButton.visible) {
+ margin += revealButton.width;
+ }
+ if (externalAuthButton.visible) {
+ margin += externalAuthButton.width;
+ }
+ if (virtualKeyboardButton.visible) {
+ margin += virtualKeyboardButton.width;
+ }
+ if (enterButton.visible) {
+ margin += enterButton.width + 2;
+ }
+ return margin;
+ }
+ opacity: 0
+ focus: true
+ echoMode: GreeterState.showPasswordInput ? (parent.showPassword ? TextInput.Normal : TextInput.Password) : TextInput.Normal
+ onTextChanged: {
+ if (syncingFromState)
+ return;
+ if (GreeterState.showPasswordInput) {
+ GreeterState.passwordBuffer = text;
+ if (!text || text.length === 0)
+ root.passwordSubmitRequested = false;
+ } else {
+ GreeterState.usernameInput = text;
+ }
+ }
+ onAccepted: {
+ if (GreeterState.showPasswordInput) {
+ root.startAuthSession(true);
+ } else {
+ if (text.trim()) {
+ root.submitUsername(text);
+ syncingFromState = true;
+ text = "";
+ syncingFromState = false;
+ }
+ }
+ }
+
+ Component.onCompleted: {
+ syncingFromState = true;
+ text = GreeterState.showPasswordInput ? GreeterState.passwordBuffer : GreeterState.usernameInput;
+ syncingFromState = false;
+ if (isPrimaryScreen && !powerMenu.isVisible)
+ forceActiveFocus();
+ }
+ onVisibleChanged: {
+ if (visible && isPrimaryScreen && !powerMenu.isVisible)
+ forceActiveFocus();
+ }
+ }
+
+ KeyboardController {
+ id: keyboard_controller
+ target: inputField
+ rootObject: root
+ }
+
+ StyledText {
+ id: placeholder
+
+ anchors.left: lockIcon.right
+ anchors.leftMargin: Theme.spacingM
+ anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))))
+ anchors.rightMargin: 2
+ anchors.verticalCenter: parent.verticalCenter
+ text: {
+ if (GreeterState.unlocking) {
+ return I18n.tr("Logging in...");
+ }
+ if (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse) {
+ return I18n.tr("Authenticating...");
+ }
+ if (GreeterState.showPasswordInput) {
+ return I18n.tr("Password...");
+ }
+ return I18n.tr("Username...");
+ }
+ color: (GreeterState.unlocking || (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse)) ? Theme.primary : Theme.outline
+ font.pixelSize: Theme.fontSizeMedium
+ opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : GreeterState.usernameInput.length === 0) ? 1 : 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+
+ StyledText {
+ anchors.left: lockIcon.right
+ anchors.leftMargin: Theme.spacingM
+ anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))))
+ anchors.rightMargin: 2
+ anchors.verticalCenter: parent.verticalCenter
+ text: {
+ if (GreeterState.showPasswordInput) {
+ if (parent.showPassword) {
+ return GreeterState.passwordBuffer;
+ }
+ return "•".repeat(GreeterState.passwordBuffer.length);
+ }
+ return GreeterState.usernameInput;
+ }
+ color: Theme.surfaceText
+ font.pixelSize: (GreeterState.showPasswordInput && !parent.showPassword) ? Theme.fontSizeLarge : Theme.fontSizeMedium
+ opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length > 0 : GreeterState.usernameInput.length > 0) ? 1 : 0
+ clip: true
+ elide: Text.ElideNone
+ horizontalAlignment: implicitWidth > width ? Text.AlignRight : Text.AlignLeft
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+
+ DankActionButton {
+ id: revealButton
+
+ anchors.right: externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))
+ anchors.rightMargin: 0
+ anchors.verticalCenter: parent.verticalCenter
+ iconName: parent.showPassword ? "visibility_off" : "visibility"
+ buttonSize: 32
+ visible: GreeterState.showPasswordInput && GreeterState.passwordBuffer.length > 0 && (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
+ enabled: visible
+ onClicked: parent.showPassword = !parent.showPassword
+ }
+ DankActionButton {
+ id: externalAuthButton
+
+ anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)
+ anchors.rightMargin: 0
+ anchors.verticalCenter: parent.verticalCenter
+ iconName: root.greeterPamHasFprint ? "fingerprint" : "key"
+ buttonSize: 32
+ visible: GreeterState.showPasswordInput && root.greeterExternalAuthAvailable && GreeterState.passwordBuffer.length === 0 && (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
+ enabled: visible
+ onClicked: root.startAuthSession(false)
+ }
+ DankActionButton {
+ id: virtualKeyboardButton
+
+ anchors.right: enterButton.visible ? enterButton.left : parent.right
+ anchors.rightMargin: enterButton.visible ? 0 : Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ iconName: "keyboard"
+ buttonSize: 32
+ visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
+ enabled: visible
+ onClicked: {
+ if (keyboard_controller.isKeyboardActive) {
+ keyboard_controller.hide();
+ } else {
+ keyboard_controller.show();
+ }
+ }
+ }
+
+ DankActionButton {
+ id: enterButton
+
+ anchors.right: parent.right
+ anchors.rightMargin: 2
+ anchors.verticalCenter: parent.verticalCenter
+ iconName: "keyboard_return"
+ buttonSize: 36
+ visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
+ enabled: true
+ onClicked: {
+ if (GreeterState.showPasswordInput) {
+ root.startAuthSession(true);
+ } else {
+ if (inputField.text.trim()) {
+ root.submitUsername(inputField.text);
+ inputField.text = "";
+ }
+ }
+ }
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+
+ Behavior on border.color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+ }
+
+ StyledText {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 38
+ Layout.topMargin: -Theme.spacingS
+ Layout.bottomMargin: -Theme.spacingS
+ text: root.authFeedbackMessage
+ color: Theme.error
+ font.pixelSize: Theme.fontSizeSmall
+ horizontalAlignment: Text.AlignHCenter
+ wrapMode: Text.WordWrap
+ maximumLineCount: 2
+ opacity: root.authFeedbackMessage !== "" ? 1 : 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.alignment: Qt.AlignHCenter
+ Layout.topMargin: 0
+ Layout.preferredWidth: switchUserRow.width + Theme.spacingL * 2
+ Layout.preferredHeight: 40
+ radius: Theme.cornerRadius
+ color: Theme.surfaceContainer
+ opacity: GreeterState.showPasswordInput ? 1 : 0
+ enabled: GreeterState.showPasswordInput
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Row {
+ id: switchUserRow
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "people"
+ size: Theme.iconSize - 4
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("Switch User")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ StateLayer {
+ stateColor: Theme.primary
+ cornerRadius: parent.radius
+ enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput
+ onClicked: {
+ GreeterState.reset();
+ root.externalAuthAutoStartedForUser = "";
+ inputField.text = "";
+ PortalService.profileImage = "";
+ }
+ }
+ }
+ }
+ }
+
+ Row {
+ anchors.top: parent.top
+ anchors.right: parent.right
+ anchors.margins: Theme.spacingXL
+ spacing: Theme.spacingL
+
+ Item {
+ width: keyboardLayoutRow.width
+ height: keyboardLayoutRow.height
+ anchors.verticalCenter: parent.verticalCenter
+ visible: {
+ if (CompositorService.isNiri) {
+ return NiriService.keyboardLayoutNames.length > 1;
+ } else if (CompositorService.isHyprland) {
+ return hyprlandLayoutCount > 1;
+ }
+ return false;
+ }
+
+ Row {
+ id: keyboardLayoutRow
+ spacing: 4
+
+ Item {
+ width: Theme.iconSize
+ height: Theme.iconSize
+
+ DankIcon {
+ name: "keyboard"
+ size: Theme.iconSize
+ color: "white"
+ anchors.centerIn: parent
+ }
+ }
+
+ Item {
+ width: childrenRect.width
+ height: Theme.iconSize
+
+ StyledText {
+ text: {
+ if (CompositorService.isNiri) {
+ const layout = NiriService.getCurrentKeyboardLayoutName();
+ if (!layout)
+ return "";
+ const parts = layout.split(" ");
+ if (parts.length > 0) {
+ return parts[0].substring(0, 2).toUpperCase();
+ }
+ return layout.substring(0, 2).toUpperCase();
+ } else if (CompositorService.isHyprland) {
+ return hyprlandCurrentLayout;
+ }
+ return "";
+ }
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Light
+ color: "white"
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+
+ MouseArea {
+ id: keyboardLayoutArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (CompositorService.isNiri) {
+ NiriService.cycleKeyboardLayout();
+ } else if (CompositorService.isHyprland) {
+ Quickshell.execDetached(["hyprctl", "switchxkblayout", hyprlandKeyboard, "next"]);
+ updateHyprlandLayout();
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ width: 1
+ height: 24
+ color: Qt.rgba(255, 255, 255, 0.2)
+ anchors.verticalCenter: parent.verticalCenter
+ visible: {
+ const keyboardVisible = (CompositorService.isNiri && NiriService.keyboardLayoutNames.length > 1) || (CompositorService.isHyprland && hyprlandLayoutCount > 1);
+ return keyboardVisible && GreetdSettings.weatherEnabled && WeatherService.weather.available;
+ }
+ }
+
+ Row {
+ spacing: 6
+ visible: GreetdSettings.weatherEnabled && WeatherService.weather.available
+ anchors.verticalCenter: parent.verticalCenter
+
+ DankIcon {
+ name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
+ size: Theme.iconSize
+ color: "white"
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: (GreetdSettings.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°"
+ font.pixelSize: Theme.fontSizeLarge
+ font.weight: Font.Light
+ color: "white"
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Rectangle {
+ width: 1
+ height: 24
+ color: Qt.rgba(255, 255, 255, 0.2)
+ anchors.verticalCenter: parent.verticalCenter
+ visible: GreetdSettings.weatherEnabled && WeatherService.weather.available && (NetworkService.networkStatus !== "disconnected" || BluetoothService.enabled || (AudioService.sink && AudioService.sink.audio) || BatteryService.batteryAvailable)
+ }
+
+ Row {
+ spacing: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ visible: NetworkService.networkStatus !== "disconnected" || (BluetoothService.available && BluetoothService.enabled) || (AudioService.sink && AudioService.sink.audio)
+
+ DankIcon {
+ name: NetworkService.networkStatus === "ethernet" ? "lan" : NetworkService.wifiSignalIcon
+ size: Theme.iconSize - 2
+ color: NetworkService.networkStatus !== "disconnected" ? "white" : Qt.rgba(255, 255, 255, 0.5)
+ anchors.verticalCenter: parent.verticalCenter
+ visible: NetworkService.networkStatus !== "disconnected"
+ }
+
+ DankIcon {
+ name: "bluetooth"
+ size: Theme.iconSize - 2
+ color: "white"
+ anchors.verticalCenter: parent.verticalCenter
+ visible: BluetoothService.available && BluetoothService.enabled
+ }
+
+ DankIcon {
+ name: {
+ if (!AudioService.sink?.audio) {
+ return "volume_up";
+ }
+ if (AudioService.sink.audio.muted)
+ return "volume_off";
+ if (AudioService.sink.audio.volume === 0)
+ return "volume_mute";
+ if (AudioService.sink.audio.volume * 100 < 33) {
+ return "volume_down";
+ }
+ return "volume_up";
+ }
+ size: Theme.iconSize - 2
+ color: (AudioService.sink && AudioService.sink.audio && (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0)) ? Qt.rgba(255, 255, 255, 0.5) : "white"
+ anchors.verticalCenter: parent.verticalCenter
+ visible: AudioService.sink && AudioService.sink.audio
+ }
+ }
+
+ Rectangle {
+ width: 1
+ height: 24
+ color: Qt.rgba(255, 255, 255, 0.2)
+ anchors.verticalCenter: parent.verticalCenter
+ visible: BatteryService.batteryAvailable && (NetworkService.networkStatus !== "disconnected" || BluetoothService.enabled || (AudioService.sink && AudioService.sink.audio))
+ }
+
+ Row {
+ spacing: 4
+ visible: BatteryService.batteryAvailable
+ anchors.verticalCenter: parent.verticalCenter
+
+ DankIcon {
+ name: {
+ if (BatteryService.isCharging) {
+ if (BatteryService.batteryLevel >= 90) {
+ return "battery_charging_full";
+ }
+
+ if (BatteryService.batteryLevel >= 80) {
+ return "battery_charging_90";
+ }
+
+ if (BatteryService.batteryLevel >= 60) {
+ return "battery_charging_80";
+ }
+
+ if (BatteryService.batteryLevel >= 50) {
+ return "battery_charging_60";
+ }
+
+ if (BatteryService.batteryLevel >= 30) {
+ return "battery_charging_50";
+ }
+
+ if (BatteryService.batteryLevel >= 20) {
+ return "battery_charging_30";
+ }
+
+ return "battery_charging_20";
+ }
+ if (BatteryService.isPluggedIn) {
+ if (BatteryService.batteryLevel >= 90) {
+ return "battery_charging_full";
+ }
+
+ if (BatteryService.batteryLevel >= 80) {
+ return "battery_charging_90";
+ }
+
+ if (BatteryService.batteryLevel >= 60) {
+ return "battery_charging_80";
+ }
+
+ if (BatteryService.batteryLevel >= 50) {
+ return "battery_charging_60";
+ }
+
+ if (BatteryService.batteryLevel >= 30) {
+ return "battery_charging_50";
+ }
+
+ if (BatteryService.batteryLevel >= 20) {
+ return "battery_charging_30";
+ }
+
+ return "battery_charging_20";
+ }
+ if (BatteryService.batteryLevel >= 95) {
+ return "battery_full";
+ }
+
+ if (BatteryService.batteryLevel >= 85) {
+ return "battery_6_bar";
+ }
+
+ if (BatteryService.batteryLevel >= 70) {
+ return "battery_5_bar";
+ }
+
+ if (BatteryService.batteryLevel >= 55) {
+ return "battery_4_bar";
+ }
+
+ if (BatteryService.batteryLevel >= 40) {
+ return "battery_3_bar";
+ }
+
+ if (BatteryService.batteryLevel >= 25) {
+ return "battery_2_bar";
+ }
+
+ return "battery_1_bar";
+ }
+ size: Theme.iconSize
+ color: {
+ if (BatteryService.isLowBattery && !BatteryService.isCharging) {
+ return Theme.error;
+ }
+
+ if (BatteryService.isCharging || BatteryService.isPluggedIn) {
+ return Theme.primary;
+ }
+
+ return "white";
+ }
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: BatteryService.batteryLevel + "%"
+ font.pixelSize: Theme.fontSizeLarge
+ font.weight: Font.Light
+ color: "white"
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+
+ DankActionButton {
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.margins: Theme.spacingXL
+ visible: GreetdSettings.lockScreenShowPowerActions
+ iconName: "power_settings_new"
+ iconColor: Theme.error
+ buttonSize: 40
+ onClicked: powerMenu.show()
+ }
+
+ Item {
+ anchors.bottom: parent.bottom
+ anchors.right: parent.right
+ anchors.margins: Theme.spacingXL
+ width: Math.max(200, currentSessionMetrics.width + 80)
+ height: 60
+
+ StyledTextMetrics {
+ id: currentSessionMetrics
+ text: root.currentSessionName
+ }
+
+ property real longestSessionWidth: {
+ let maxWidth = 0;
+ for (var i = 0; i < sessionMetricsRepeater.count; i++) {
+ const item = sessionMetricsRepeater.itemAt(i);
+ if (item && item.width > maxWidth) {
+ maxWidth = item.width;
+ }
+ }
+ return maxWidth;
+ }
+
+ Repeater {
+ id: sessionMetricsRepeater
+ model: GreeterState.sessionList
+ delegate: StyledTextMetrics {
+ text: modelData
+ }
+ }
+
+ DankDropdown {
+ id: sessionDropdown
+ anchors.fill: parent
+ text: ""
+ description: ""
+ currentValue: root.currentSessionName
+ options: GreeterState.sessionList
+ enableFuzzySearch: GreeterState.sessionList.length > 5
+ popupWidthOffset: 0
+ popupWidth: Math.max(250, parent.longestSessionWidth + 100)
+ openUpwards: true
+ alignPopupRight: true
+ onValueChanged: value => {
+ const idx = GreeterState.sessionList.indexOf(value);
+ if (idx < 0)
+ return;
+ GreeterState.currentSessionIndex = idx;
+ GreeterState.selectedSession = GreeterState.sessionExecs[idx];
+ GreeterState.selectedSessionPath = GreeterState.sessionPaths[idx];
+ }
+ }
+ }
+ }
+
+ property string currentSessionName: GreeterState.sessionList[GreeterState.currentSessionIndex] || ""
+
+ function finalizeSessionSelection() {
+ if (GreeterState.sessionList.length === 0)
+ return;
+ if (!GreetdMemory.memoryReady)
+ return;
+ if (!GreetdSettings.settingsLoaded)
+ return;
+
+ const savedSession = GreetdSettings.rememberLastSession ? GreetdMemory.lastSessionId : "";
+ if (savedSession && GreetdSettings.rememberLastSession) {
+ for (var i = 0; i < GreeterState.sessionPaths.length; i++) {
+ if (GreeterState.sessionPaths[i] === savedSession) {
+ GreeterState.currentSessionIndex = i;
+ GreeterState.selectedSession = GreeterState.sessionExecs[i] || "";
+ GreeterState.selectedSessionPath = GreeterState.sessionPaths[i];
+ return;
+ }
+ }
+ }
+
+ GreeterState.currentSessionIndex = 0;
+ GreeterState.selectedSession = GreeterState.sessionExecs[0] || "";
+ GreeterState.selectedSessionPath = GreeterState.sessionPaths[0] || "";
+ }
+
+ property var sessionDirs: {
+ const homeDir = Quickshell.env("HOME") || "";
+ const dirs = ["/usr/share/wayland-sessions", "/usr/share/xsessions", "/usr/local/share/wayland-sessions", "/usr/local/share/xsessions"];
+
+ if (homeDir) {
+ dirs.push(homeDir + "/.local/share/wayland-sessions");
+ dirs.push(homeDir + "/.local/share/xsessions");
+ }
+
+ if (xdgDataDirs) {
+ xdgDataDirs.split(":").forEach(dir => {
+ if (dir) {
+ dirs.push(dir + "/wayland-sessions");
+ dirs.push(dir + "/xsessions");
+ }
+ });
+ }
+ return dirs;
+ }
+
+ property var _pendingFiles: ({})
+ property int _pendingCount: 0
+
+ function _addSession(path, name, exec) {
+ if (!name || !exec || GreeterState.sessionList.includes(name))
+ return;
+ GreeterState.sessionList = GreeterState.sessionList.concat([name]);
+ GreeterState.sessionExecs = GreeterState.sessionExecs.concat([exec]);
+ GreeterState.sessionPaths = GreeterState.sessionPaths.concat([path]);
+ }
+
+ function _parseDesktopFile(content, path) {
+ let name = "";
+ let exec = "";
+ const lines = content.split("\n");
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+ if (!name && line.startsWith("Name="))
+ name = line.substring(5).trim();
+ else if (!exec && line.startsWith("Exec="))
+ exec = line.substring(5).trim();
+ if (name && exec)
+ break;
+ }
+ _addSession(path, name, exec);
+ }
+
+ function _loadDesktopFile(filePath) {
+ if (_pendingFiles[filePath])
+ return;
+ _pendingFiles[filePath] = true;
+ _pendingCount++;
+
+ const loader = desktopFileLoader.createObject(root, {
+ "filePath": filePath
+ });
+ }
+
+ function _onFileLoaded(filePath) {
+ _pendingCount--;
+ if (_pendingCount === 0)
+ Qt.callLater(finalizeSessionSelection);
+ }
+
+ Component {
+ id: desktopFileLoader
+
+ FileView {
+ id: fv
+ property string filePath: ""
+ path: filePath
+
+ onLoaded: {
+ root._parseDesktopFile(text(), filePath);
+ root._onFileLoaded(filePath);
+ fv.destroy();
+ }
+
+ onLoadFailed: {
+ root._onFileLoaded(filePath);
+ fv.destroy();
+ }
+ }
+ }
+
+ Repeater {
+ model: isPrimaryScreen ? sessionDirs : []
+
+ Item {
+ required property string modelData
+
+ FolderListModel {
+ folder: encodeFileUrl(modelData)
+ nameFilters: ["*.desktop"]
+ showDirs: false
+ showDotAndDotDot: false
+
+ onStatusChanged: {
+ if (status !== FolderListModel.Ready)
+ return;
+ for (let i = 0; i < count; i++) {
+ let fp = get(i, "filePath");
+ if (fp.startsWith("file://"))
+ fp = fp.substring(7);
+ root._loadDesktopFile(fp);
+ }
+ }
+ }
+ }
+ }
+
+ Connections {
+ target: Greetd
+ enabled: isPrimaryScreen
+
+ function onAuthMessage(message, error, responseRequired, echoResponse) {
+ if (responseRequired) {
+ cancelingExternalAuthForPassword = false;
+ passwordSessionTransitionRetryCount = 0;
+ awaitingExternalAuth = false;
+ pendingPasswordResponse = true;
+ const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
+ if (!passwordSubmitRequested && hasPasswordBuffer)
+ passwordSubmitRequested = true;
+ if (passwordSubmitRequested && !root.submitBufferedPassword())
+ passwordSubmitRequested = false;
+ if (passwordSubmitRequested || hasPasswordBuffer) {
+ authTimeout.interval = defaultAuthTimeoutMs;
+ authTimeout.restart();
+ } else {
+ authTimeout.stop();
+ }
+ return;
+ }
+ pendingPasswordResponse = false;
+ const externalPrompt = root.isExternalAuthPrompt(message, responseRequired);
+ if (!passwordSubmitRequested)
+ awaitingExternalAuth = root.greeterExternalAuthAvailable && externalPrompt;
+ if (awaitingExternalAuth || (passwordSubmitRequested && externalPrompt && root.greeterPamHasExternalAuth))
+ authTimeout.interval = externalAuthTimeoutMs;
+ else
+ authTimeout.interval = defaultAuthTimeoutMs;
+ authTimeout.restart();
+ Greetd.respond("");
+ }
+
+ function onStateChanged() {
+ if (Greetd.state === GreetdState.Inactive) {
+ const resumePasswordSubmit = cancelingExternalAuthForPassword && passwordSubmitRequested;
+ awaitingExternalAuth = false;
+ pendingPasswordResponse = false;
+ cancelingExternalAuthForPassword = false;
+ authTimeout.interval = defaultAuthTimeoutMs;
+ authTimeout.stop();
+ if (resumePasswordSubmit) {
+ Qt.callLater(function() {
+ root.startAuthSession(true);
+ });
+ return;
+ }
+ resetPasswordSessionTransition(true);
+ }
+ }
+
+ function onReadyToLaunch() {
+ awaitingExternalAuth = false;
+ pendingPasswordResponse = false;
+ resetPasswordSessionTransition(true);
+ authTimeout.interval = defaultAuthTimeoutMs;
+ authTimeout.stop();
+ passwordFailureCount = 0;
+ clearAuthFeedback();
+ const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex];
+ const sessionPath = GreeterState.selectedSessionPath || GreeterState.sessionPaths[GreeterState.currentSessionIndex];
+ if (!sessionCmd) {
+ GreeterState.pamState = "error";
+ authFeedbackMessage = currentAuthMessage();
+ placeholderDelay.restart();
+ return;
+ }
+
+ GreeterState.unlocking = true;
+ launchTimeout.restart();
+ if (GreetdSettings.rememberLastSession) {
+ GreetdMemory.setLastSessionId(sessionPath);
+ } else if (GreetdMemory.lastSessionId) {
+ GreetdMemory.setLastSessionId("");
+ }
+ if (GreetdSettings.rememberLastUser) {
+ GreetdMemory.setLastSuccessfulUser(GreeterState.username);
+ } else if (GreetdMemory.lastSuccessfulUser) {
+ GreetdMemory.setLastSuccessfulUser("");
+ }
+ pendingLaunchCommand = sessionCmd;
+ pendingLaunchEnv = ["XDG_SESSION_TYPE=wayland"];
+ memoryFlushTimer.restart();
+ }
+
+ function onAuthFailure(message) {
+ awaitingExternalAuth = false;
+ pendingPasswordResponse = false;
+ resetPasswordSessionTransition(true);
+ authTimeout.interval = defaultAuthTimeoutMs;
+ authTimeout.stop();
+ launchTimeout.stop();
+ GreeterState.unlocking = false;
+ if (isLikelyLockoutMessage(message)) {
+ GreeterState.pamState = "max";
+ } else {
+ GreeterState.pamState = "fail";
+ passwordFailureCount = passwordFailureCount + 1;
+ }
+ authFeedbackMessage = currentAuthMessage();
+ GreeterState.passwordBuffer = "";
+ inputField.text = "";
+ placeholderDelay.restart();
+ Greetd.cancelSession();
+ }
+
+ function onError(error) {
+ awaitingExternalAuth = false;
+ pendingPasswordResponse = false;
+ resetPasswordSessionTransition(true);
+ authTimeout.interval = defaultAuthTimeoutMs;
+ authTimeout.stop();
+ launchTimeout.stop();
+ GreeterState.unlocking = false;
+ GreeterState.pamState = "error";
+ authFeedbackMessage = currentAuthMessage();
+ GreeterState.passwordBuffer = "";
+ inputField.text = "";
+ placeholderDelay.restart();
+ Greetd.cancelSession();
+ }
+ }
+
+ Timer {
+ id: memoryFlushTimer
+ interval: memoryFlushDelayMs
+ onTriggered: {
+ if (!pendingLaunchCommand)
+ return;
+ const sessionCommand = pendingLaunchCommand;
+ const launchEnv = pendingLaunchEnv;
+ pendingLaunchCommand = "";
+ pendingLaunchEnv = [];
+ Greetd.launch(sessionCommand.split(" "), launchEnv);
+ }
+ }
+
+ Timer {
+ id: authTimeout
+ interval: defaultAuthTimeoutMs
+ onTriggered: {
+ if (GreeterState.unlocking || Greetd.state === GreetdState.Inactive)
+ return;
+ awaitingExternalAuth = false;
+ pendingPasswordResponse = false;
+ resetPasswordSessionTransition(true);
+ authTimeout.interval = defaultAuthTimeoutMs;
+ GreeterState.pamState = "error";
+ authFeedbackMessage = currentAuthMessage();
+ GreeterState.passwordBuffer = "";
+ inputField.text = "";
+ placeholderDelay.restart();
+ Greetd.cancelSession();
+ }
+ }
+
+ Timer {
+ id: launchTimeout
+ interval: 8000
+ onTriggered: {
+ if (!GreeterState.unlocking)
+ return;
+ pendingPasswordResponse = false;
+ resetPasswordSessionTransition(true);
+ GreeterState.unlocking = false;
+ GreeterState.pamState = "error";
+ authFeedbackMessage = currentAuthMessage();
+ placeholderDelay.restart();
+ Greetd.cancelSession();
+ }
+ }
+
+ Timer {
+ id: placeholderDelay
+ interval: 4000
+ onTriggered: clearAuthFeedback()
+ }
+
+ LockPowerMenu {
+ id: powerMenu
+ showLogout: false
+ powerActionConfirmOverride: GreetdSettings.powerActionConfirm
+ powerActionHoldDurationOverride: GreetdSettings.powerActionHoldDuration
+ powerMenuActionsOverride: GreetdSettings.powerMenuActions
+ powerMenuDefaultActionOverride: GreetdSettings.powerMenuDefaultAction
+ powerMenuGridLayoutOverride: GreetdSettings.powerMenuGridLayout
+ requiredActions: ["poweroff"]
+ onClosed: {
+ if (isPrimaryScreen && inputField && inputField.forceActiveFocus) {
+ Qt.callLater(() => inputField.forceActiveFocus());
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreeterState.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreeterState.qml
new file mode 100644
index 0000000..ac57de7
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreeterState.qml
@@ -0,0 +1,30 @@
+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 pamState: ""
+ property bool unlocking: false
+
+ property var sessionList: []
+ property var sessionExecs: []
+ property var sessionPaths: []
+ property int currentSessionIndex: 0
+
+ function reset() {
+ showPasswordInput = false;
+ username = "";
+ usernameInput = "";
+ passwordBuffer = "";
+ pamState = "";
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreeterSurface.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreeterSurface.qml
new file mode 100644
index 0000000..a8af5df
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreeterSurface.qml
@@ -0,0 +1,34 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell
+import Quickshell.Wayland
+import Quickshell.Services.Greetd
+import qs.Common
+
+Variants {
+ model: Quickshell.screens
+
+ PanelWindow {
+ id: root
+
+ property var modelData
+
+ screen: modelData
+ anchors {
+ left: true
+ right: true
+ top: true
+ bottom: true
+ }
+ exclusionMode: ExclusionMode.Normal
+ WlrLayershell.layer: WlrLayer.Overlay
+ WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
+ color: "transparent"
+
+ GreeterContent {
+ anchors.fill: parent
+ screenName: root.screen?.name ?? ""
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/README.md b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/README.md
new file mode 100644
index 0000000..4ac5a77
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/README.md
@@ -0,0 +1,277 @@
+# 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
+```
+
+#### Syncing themes (Optional)
+
+To sync your wallpaper and theme with the greeter login screen, follow the manual setup below:
+
+<details>
+<summary>Manual theme syncing</summary>
+
+```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
+- 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.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:** Run `dms greeter sync` to automatically sync your DMS theme with the greeter.
+
+**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>`.
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/dms-greeter b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/dms-greeter
new file mode 100755
index 0000000..5d3419c
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/dms-greeter
@@ -0,0 +1,424 @@
+#!/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
+}
+
+exec_compositor() {
+ local log_tag="$1"
+ shift
+
+ if [[ "$DEBUG_MODE" == "1" ]]; then
+ exec "$@"
+ fi
+
+ if command -v systemd-cat >/dev/null 2>&1; then
+ exec "$@" > >(systemd-cat -t "dms-greeter/$log_tag" -p info) 2>&1
+ fi
+
+ exec "$@"
+}
+
+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"
+
+
+# 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
+
+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"
+}
+
+debug {
+ keep-max-bpc-unchanged
+}
+
+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 [[ -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 -d quit"
+ else
+ exec_compositor "mango" mango -s "$QS_CMD && mmsg -d quit"
+ fi
+ ;;
+
+ *)
+ echo "Error: Unsupported compositor: $COMPOSITOR" >&2
+ echo "Supported compositors: niri, hyprland, sway, scroll, miracle, mango, labwc" >&2
+ exit 1
+ ;;
+esac
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/dms-hypr.conf b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/dms-hypr.conf
new file mode 100644
index 0000000..33eefcd
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/dms-hypr.conf
@@ -0,0 +1,3 @@
+env = DMS_RUN_GREETER,1
+
+exec = sh -c "qs -p _DMS_PATH_; hyprctl dispatch exit"
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/dms-niri.kdl b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/dms-niri.kdl
new file mode 100644
index 0000000..dfc4703
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/dms-niri.kdl
@@ -0,0 +1,23 @@
+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"
+
+debug {
+ keep-max-bpc-unchanged
+}
+
+gestures {
+ hot-corners {
+ off
+ }
+}
+
+layout {
+ background-color "#000000"
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/greet-hyprland.sh b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/greet-hyprland.sh
new file mode 100755
index 0000000..c23b479
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/greet-hyprland.sh
@@ -0,0 +1,11 @@
+#!/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.conf
+else
+ exec Hyprland -c /etc/greetd/dms-hypr.conf
+fi
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/greet-niri.sh b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/greet-niri.sh
new file mode 100755
index 0000000..d31db65
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/greet-niri.sh
@@ -0,0 +1,8 @@
+#!/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