From 70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 Mon Sep 17 00:00:00 2001 From: AlexanderCurl Date: Sat, 18 Apr 2026 17:46:06 +0100 Subject: Replaced file structures for themes in the correct format --- .../quickshell/Modules/Greetd/GreetdEnv.js | 20 + .../quickshell/Modules/Greetd/GreetdMemory.qml | 128 ++ .../quickshell/Modules/Greetd/GreetdSettings.qml | 199 ++ .../quickshell/Modules/Greetd/GreeterContent.qml | 1908 ++++++++++++++++++++ .../quickshell/Modules/Greetd/GreeterState.qml | 30 + .../quickshell/Modules/Greetd/GreeterSurface.qml | 34 + .../quickshell/Modules/Greetd/README.md | 277 +++ .../quickshell/Modules/Greetd/assets/dms-greeter | 424 +++++ .../quickshell/Modules/Greetd/assets/dms-hypr.conf | 3 + .../quickshell/Modules/Greetd/assets/dms-niri.kdl | 23 + .../Modules/Greetd/assets/greet-hyprland.sh | 11 + .../quickshell/Modules/Greetd/assets/greet-niri.sh | 8 + 12 files changed, 3065 insertions(+) create mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreetdEnv.js create mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreetdMemory.qml create mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreetdSettings.qml create mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreeterContent.qml create mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreeterState.qml create mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreeterSurface.qml create mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/README.md create mode 100755 raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/dms-greeter create mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/dms-hypr.conf create mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/dms-niri.kdl create mode 100755 raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/greet-hyprland.sh create mode 100755 raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/greet-niri.sh (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd') diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreetdEnv.js b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreetdEnv.js new file mode 100644 index 0000000..fcce181 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreetdMemory.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreetdMemory.qml new file mode 100644 index 0000000..da949d3 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreetdSettings.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreetdSettings.qml new file mode 100644 index 0000000..e1f670c --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreeterContent.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreeterContent.qml new file mode 100644 index 0000000..fa82315 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreeterState.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreeterState.qml new file mode 100644 index 0000000..ac57de7 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreeterSurface.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreeterSurface.qml new file mode 100644 index 0000000..a8af5df --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/README.md b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/README.md new file mode 100644 index 0000000..4ac5a77 --- /dev/null +++ b/raveos-hyprland-theme/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: + +
+Manual theme syncing + +```bash +# Add yourself to greeter group +sudo usermod -aG greeter + +# Set ACLs to allow greeter to traverse your directories +setfacl -m u:greeter:x ~ ~/.config ~/.local ~/.cache ~/.local/state + +# Set group ownership on config directories +sudo chgrp -R greeter ~/.config/DankMaterialShell +sudo chgrp -R greeter ~/.local/state/DankMaterialShell +sudo chgrp -R greeter ~/.cache/DankMaterialShell +sudo chmod -R g+rX ~/.config/DankMaterialShell ~/.cache/DankMaterialShell ~/.cache/quickshell + +# Create symlinks +sudo ln -sf ~/.config/DankMaterialShell/settings.json /var/cache/dms-greeter/settings.json +sudo ln -sf ~/.local/state/DankMaterialShell/session.json /var/cache/dms-greeter/session.json +sudo ln -sf ~/.cache/DankMaterialShell/dms-colors.json /var/cache/dms-greeter/colors.json + +# Logout and login for group membership to take effect +``` + +
+ +### Fedora / RHEL / Rocky / Alma + +Install from COPR or build the RPM: + +```bash +# From COPR (when available) +sudo dnf copr enable avenge/dms +sudo dnf install dms-greeter + +# Or build locally +cd /path/to/DankMaterialShell +rpkg local +sudo rpm -ivh x86_64/dms-greeter-*.rpm +``` + +The package automatically: +- Creates the greeter user +- Sets up directories and permissions +- Configures greetd with auto-detected compositor +- Applies SELinux contexts + +Then complete setup: + +```bash +dms greeter enable +dms greeter sync +``` + +#### Syncing themes (Optional) + +Run: + +```bash +dms greeter sync +``` + +Then logout/login to see your wallpaper on the greeter. + +### Automatic + +The easiest thing is to run `dms greeter install` or `dms` for interactive installation. +On Debian/openSUSE, this now prefers the `dms-greeter` package when the OBS repo is configured. + +### Manual (fallback only) + +Use this only if no package is available for your distro. + +1. Install `greetd` (in most distro's standard repositories) and `quickshell` + +2. Create the greeter user (if not already created by greetd): +```bash +sudo groupadd -r greeter +sudo useradd -r -g greeter -d /var/lib/greeter -s /bin/bash -c "System Greeter" greeter +sudo mkdir -p /var/lib/greeter +sudo chown greeter:greeter /var/lib/greeter +``` + +3. Clone the dms project to `/etc/xdg/quickshell/dms-greeter`: +```bash +sudo git clone https://github.com/AvengeMedia/DankMaterialShell.git /etc/xdg/quickshell/dms-greeter +``` + +4. Copy `Modules/Greetd/assets/dms-greeter` to `/usr/local/bin/dms-greeter`: +```bash +sudo cp /etc/xdg/quickshell/dms-greeter/Modules/Greetd/assets/dms-greeter /usr/local/bin/dms-greeter +sudo chmod +x /usr/local/bin/dms-greeter +``` + +5. Create greeter cache directory with proper permissions: +```bash +sudo mkdir -p /var/cache/dms-greeter +sudo chown : /var/cache/dms-greeter +sudo chmod 2770 /var/cache/dms-greeter +``` + +6. Edit or create `/etc/greetd/config.toml`: +```toml +[terminal] +vt = 1 + +[default_session] +user = "greeter" +# Change compositor to another wrapper-supported compositor if preferred +command = "/usr/local/bin/dms-greeter --command niri" +``` + +7. Disable existing display manager and enable greetd: +```bash +sudo systemctl disable gdm sddm lightdm +sudo systemctl enable greetd +``` + +8. (Optional) Set up theme syncing using the manual ACL method described in the Configuration → Personalization section below + +#### Legacy installation (deprecated) + +If you prefer the old method with separate shell scripts and config files: +1. Copy `assets/dms-niri.kdl` or `assets/dms-hypr.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 `:` with `2770` permissions. If the greeter user is not available yet, DMS falls back to `root:`. diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/dms-greeter b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/dms-greeter new file mode 100755 index 0000000..5d3419c --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/dms-hypr.conf b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/dms-hypr.conf new file mode 100644 index 0000000..33eefcd --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/dms-niri.kdl b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/dms-niri.kdl new file mode 100644 index 0000000..dfc4703 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/greet-hyprland.sh b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/greet-hyprland.sh new file mode 100755 index 0000000..c23b479 --- /dev/null +++ b/raveos-hyprland-theme/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-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/greet-niri.sh b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Greetd/assets/greet-niri.sh new file mode 100755 index 0000000..d31db65 --- /dev/null +++ b/raveos-hyprland-theme/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 -- cgit v1.3