1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-03 20:32:07 -04:00

refactor(greeter): Update auth flows and add configurable opts

- Finally fix debug info logs before dms greeter loads
- prevent greeter/lockscreen auth stalls with timeout recovery and unlock-state sync
This commit is contained in:
purian23
2026-03-04 14:17:56 -05:00
committed by bbedward
parent 2ff42eba41
commit 73c75fcc2c
13 changed files with 1032 additions and 281 deletions

View File

@@ -294,6 +294,8 @@ Singleton {
property string centeringMode: "index"
property string clockDateFormat: ""
property string lockDateFormat: ""
property bool greeterRememberLastSession: true
property bool greeterRememberLastUser: true
property int mediaSize: 1
property string appLauncherViewMode: "list"

View File

@@ -154,6 +154,8 @@ var SPEC = {
centeringMode: { def: "index" },
clockDateFormat: { def: "" },
lockDateFormat: { def: "" },
greeterRememberLastSession: { def: true },
greeterRememberLastUser: { def: true },
mediaSize: { def: 1 },
appLauncherViewMode: { def: "list" },

View File

@@ -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;
}

View File

@@ -4,6 +4,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import "GreetdEnv.js" as GreetdEnv
Singleton {
id: root
@@ -11,6 +12,8 @@ Singleton {
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"
readonly property string sessionConfigPath: greetCfgDir + "/session.json"
readonly property string memoryFile: greetCfgDir + "/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: ""
@@ -49,26 +52,44 @@ Singleton {
if (!content || !content.trim())
return;
const memory = JSON.parse(content);
lastSessionId = memory.lastSessionId || "";
lastSuccessfulUser = memory.lastSuccessfulUser || "";
lastSessionId = rememberLastSession ? (memory.lastSessionId || "") : "";
lastSuccessfulUser = rememberLastUser ? (memory.lastSuccessfulUser || "") : "";
if (!rememberLastSession || !rememberLastUser)
saveMemory();
} catch (e) {
console.warn("Failed to parse greetd memory:", e);
}
}
function saveMemory() {
memoryFileView.setText(JSON.stringify({
"lastSessionId": lastSessionId,
"lastSuccessfulUser": lastSuccessfulUser
}, null, 2));
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();
}

View File

@@ -5,6 +5,7 @@ import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import "GreetdEnv.js" as GreetdEnv
Singleton {
id: root
@@ -41,6 +42,8 @@ Singleton {
property string lockDateFormat: ""
property bool lockScreenShowPowerActions: true
property bool lockScreenShowProfileImage: true
property bool rememberLastSession: true
property bool rememberLastUser: true
property bool powerActionConfirm: true
property real powerActionHoldDuration: 0.5
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
@@ -52,53 +55,73 @@ Singleton {
function parseSettings(content) {
try {
let settings = {};
if (content && content.trim()) {
const settings = JSON.parse(content);
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "purple";
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "";
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;
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;
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";
settingsLoaded = true;
settings = JSON.parse(content);
}
if (typeof Theme !== "undefined") {
if (currentThemeName === "custom" && customThemeFile) {
Theme.loadCustomThemeFromFile(customThemeFile);
}
Theme.applyGreeterTheme(currentThemeName);
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 : "";
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;
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;
}
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;
}
}
@@ -133,5 +156,9 @@ Singleton {
onLoaded: {
parseSettings(settingsFile.text());
}
onLoadFailed: error => {
console.warn("Failed to load greetd settings:", error);
root.parseSettings("");
}
}
}

View File

@@ -31,6 +31,17 @@ Item {
signal launchRequested
property bool weatherInitialized: false
property bool awaitingExternalAuth: false
property int defaultAuthTimeoutMs: 12000
property int externalAuthTimeoutMs: 45000
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 faillockConfigText: ""
function initWeatherService() {
if (weatherInitialized)
@@ -44,16 +55,238 @@ Item {
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 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];
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 "Authentication error - try again";
if (GreeterState.pamState === "max")
return "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 "Incorrect password - attempt " + attempt + " of " + passwordAttemptLimitHint + " (lockout may follow)";
}
return "Incorrect password - next failures may trigger account lockout";
}
return "Incorrect password";
}
return "";
}
function clearAuthFeedback() {
GreeterState.pamState = "";
authFeedbackMessage = "";
}
Connections {
target: GreetdSettings
function onSettingsLoadedChanged() {
if (GreetdSettings.settingsLoaded)
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();
}
onLoadFailed: {
root.greetdPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: systemAuthPamWatcher
path: "/etc/pam.d/system-auth"
printErrors: false
onLoaded: {
root.systemAuthPamText = text();
root.refreshPasswordAttemptPolicyHint();
}
onLoadFailed: {
root.systemAuthPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: commonAuthPamWatcher
path: "/etc/pam.d/common-auth"
printErrors: false
onLoaded: {
root.commonAuthPamText = text();
root.refreshPasswordAttemptPolicyHint();
}
onLoadFailed: {
root.commonAuthPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: passwordAuthPamWatcher
path: "/etc/pam.d/password-auth"
printErrors: false
onLoaded: {
root.passwordAuthPamText = text();
root.refreshPasswordAttemptPolicyHint();
}
onLoadFailed: {
root.passwordAuthPamText = "";
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();
@@ -63,6 +296,8 @@ Item {
}
function applyLastSuccessfulUser() {
if (!GreetdSettings.settingsLoaded || !GreetdSettings.rememberLastUser)
return;
const lastUser = GreetdMemory.lastSuccessfulUser;
if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) {
GreeterState.username = lastUser;
@@ -72,6 +307,38 @@ Item {
}
}
function submitUsername(rawValue) {
const user = (rawValue || "").trim();
if (!user)
return;
if (GreeterState.username !== user) {
passwordFailureCount = 0;
clearAuthFeedback();
}
GreeterState.username = user;
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(user);
GreeterState.passwordBuffer = "";
}
function startAuthSession() {
if (!GreeterState.showPasswordInput || !GreeterState.username)
return;
if (GreeterState.unlocking || Greetd.state !== GreetdState.Inactive)
return;
if (!GreeterState.passwordBuffer || GreeterState.passwordBuffer.length === 0)
return;
awaitingExternalAuth = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.restart();
Greetd.createSession(GreeterState.username);
}
function isExternalAuthPrompt(message, responseRequired) {
// Non-response PAM messages commonly represent waiting states (fprint/U2F/token touch).
return !responseRequired;
}
Component.onDestruction: {
if (weatherInitialized)
WeatherService.removeRef();
@@ -421,15 +688,10 @@ Item {
}
onAccepted: {
if (GreeterState.showPasswordInput) {
if (Greetd.state === GreetdState.Inactive && GreeterState.username) {
Greetd.createSession(GreeterState.username);
}
root.startAuthSession();
} else {
if (text.trim()) {
GreeterState.username = text.trim();
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(GreeterState.username);
GreeterState.passwordBuffer = "";
root.submitUsername(text);
syncingFromState = true;
text = "";
syncingFromState = false;
@@ -568,15 +830,10 @@ Item {
enabled: true
onClicked: {
if (GreeterState.showPasswordInput) {
if (GreeterState.username) {
Greetd.createSession(GreeterState.username);
}
root.startAuthSession();
} else {
if (inputField.text.trim()) {
GreeterState.username = inputField.text.trim();
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(GreeterState.username);
GreeterState.passwordBuffer = "";
root.submitUsername(inputField.text);
inputField.text = "";
}
}
@@ -601,20 +858,16 @@ Item {
StyledText {
Layout.fillWidth: true
Layout.preferredHeight: 20
Layout.preferredHeight: 38
Layout.topMargin: -Theme.spacingS
Layout.bottomMargin: -Theme.spacingS
text: {
if (GreeterState.pamState === "error")
return "Authentication error - try again";
if (GreeterState.pamState === "fail")
return "Incorrect password";
return "";
}
text: root.authFeedbackMessage
color: Theme.error
font.pixelSize: Theme.fontSizeSmall
horizontalAlignment: Text.AlignHCenter
opacity: GreeterState.pamState !== "" ? 1 : 0
wrapMode: Text.WordWrap
maximumLineCount: 2
opacity: root.authFeedbackMessage !== "" ? 1 : 0
Behavior on opacity {
NumberAnimation {
@@ -1029,9 +1282,11 @@ Item {
return;
if (!GreetdMemory.memoryReady)
return;
if (!GreetdSettings.settingsLoaded)
return;
const savedSession = GreetdMemory.lastSessionId;
if (savedSession) {
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;
@@ -1163,45 +1418,100 @@ Item {
enabled: isPrimaryScreen
function onAuthMessage(message, error, responseRequired, echoResponse) {
awaitingExternalAuth = root.isExternalAuthPrompt(message, responseRequired);
authTimeout.interval = awaitingExternalAuth ? externalAuthTimeoutMs : defaultAuthTimeoutMs;
authTimeout.restart();
if (responseRequired) {
Greetd.respond(GreeterState.passwordBuffer);
GreeterState.passwordBuffer = "";
inputField.text = "";
return;
}
if (!error)
Greetd.respond("");
Greetd.respond("");
}
function onStateChanged() {
if (Greetd.state === GreetdState.Inactive) {
awaitingExternalAuth = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
}
}
function onReadyToLaunch() {
awaitingExternalAuth = false;
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();
GreetdMemory.setLastSessionId(sessionPath);
GreetdMemory.setLastSuccessfulUser(GreeterState.username);
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("");
}
Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"]);
}
function onAuthFailure(message) {
awaitingExternalAuth = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
launchTimeout.stop();
GreeterState.unlocking = false;
GreeterState.pamState = "fail";
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;
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: authTimeout
interval: defaultAuthTimeoutMs
onTriggered: {
if (GreeterState.unlocking || Greetd.state === GreetdState.Inactive)
return;
awaitingExternalAuth = false;
authTimeout.interval = defaultAuthTimeoutMs;
GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
GreeterState.passwordBuffer = "";
inputField.text = "";
placeholderDelay.restart();
@@ -1217,6 +1527,7 @@ Item {
return;
GreeterState.unlocking = false;
GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
placeholderDelay.restart();
Greetd.cancelSession();
}
@@ -1225,7 +1536,7 @@ Item {
Timer {
id: placeholderDelay
interval: 4000
onTriggered: GreeterState.pamState = ""
onTriggered: clearAuthFeedback()
}
LockPowerMenu {

View File

@@ -9,6 +9,7 @@ A greeter for [greetd](https://github.com/kennylevinsen/greetd) that follows the
- **Multiple compositors**: Supports niri, Hyprland, Sway, or 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
@@ -212,6 +213,7 @@ 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`:

View File

@@ -6,6 +6,9 @@ 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
@@ -22,6 +25,15 @@ Options:
(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:
@@ -30,6 +42,7 @@ Examples:
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
@@ -43,6 +56,41 @@ require_command() {
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
local log_file="$CACHE_DIR/$log_tag.log"
exec "$@" >> "$log_file" 2>&1
}
while [[ $# -gt 0 ]]; do
case $1 in
--command)
@@ -61,6 +109,26 @@ while [[ $# -gt 0 ]]; do
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
@@ -113,8 +181,38 @@ export EGL_PLATFORM=gbm
export DMS_RUN_GREETER=1
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
mkdir -p "$CACHE_DIR"
# 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
@@ -130,7 +228,9 @@ if [[ "$DMS_PATH" == /* ]]; then
else
RESOLVED_PATH=$(locate_dms_config "$DMS_PATH")
if [[ $? -eq 0 && -n "$RESOLVED_PATH" ]]; then
echo "Located DMS config at: $RESOLVED_PATH" >&2
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
@@ -192,7 +292,7 @@ NIRI_EOF
spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation"
NIRI_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
exec niri -c "$COMPOSITOR_CONFIG"
exec_compositor "niri" niri -c "$COMPOSITOR_CONFIG"
;;
hyprland)
@@ -222,9 +322,9 @@ HYPRLAND_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi
if command -v start-hyprland >/dev/null 2>&1; then
exec start-hyprland -- --config "$COMPOSITOR_CONFIG"
exec_compositor "hyprland" start-hyprland -- --config "$COMPOSITOR_CONFIG"
else
exec Hyprland -c "$COMPOSITOR_CONFIG"
exec_compositor "hyprland" Hyprland -c "$COMPOSITOR_CONFIG"
fi
;;
@@ -245,7 +345,7 @@ exec "$QS_CMD; swaymsg exit"
SWAY_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi
exec sway --unsupported-gpu -c "$COMPOSITOR_CONFIG"
exec_compositor "sway" sway --unsupported-gpu -c "$COMPOSITOR_CONFIG"
;;
scroll)
@@ -265,7 +365,7 @@ exec "$QS_CMD; scrollmsg exit"
SCROLL_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi
exec scroll -c "$COMPOSITOR_CONFIG"
exec_compositor "scroll" scroll -c "$COMPOSITOR_CONFIG"
;;
miracle|miracle-wm)
@@ -285,24 +385,24 @@ exec "$QS_CMD; miraclemsg exit"
MIRACLE_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi
exec miracle-wm -c "$COMPOSITOR_CONFIG"
exec_compositor "miracle" miracle-wm -c "$COMPOSITOR_CONFIG"
;;
labwc)
require_command "labwc"
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
exec labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD"
exec_compositor "labwc" labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD"
else
exec labwc --session "$QS_CMD"
exec_compositor "labwc" labwc --session "$QS_CMD"
fi
;;
mango|mangowc)
require_command "mango"
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
exec mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit"
exec_compositor "mango" mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit"
else
exec mango -s "$QS_CMD && mmsg -d quit"
exec_compositor "mango" mango -s "$QS_CMD && mmsg -d quit"
fi
;;

View File

@@ -745,8 +745,7 @@ Item {
}
}
onAccepted: {
if (!demoMode && !pam.passwd.active) {
console.log("Enter pressed, starting PAM authentication");
if (!demoMode && !root.unlocking && !pam.passwd.active && !pam.u2fPending) {
pam.passwd.start();
}
}
@@ -755,6 +754,11 @@ Item {
return;
}
if (root.unlocking) {
event.accepted = true;
return;
}
if (event.key === Qt.Key_Escape) {
clear();
}
@@ -998,8 +1002,7 @@ Item {
visible: (demoMode || (!pam.passwd.active && !root.unlocking))
enabled: !demoMode
onClicked: {
if (!demoMode) {
console.log("Enter button clicked, starting PAM authentication");
if (!demoMode && !root.unlocking && !pam.u2fPending) {
pam.passwd.start();
}
}
@@ -1602,6 +1605,7 @@ Item {
onStateChanged: {
root.pamState = state;
if (state !== "") {
root.unlocking = false;
placeholderDelay.restart();
passwordField.text = "";
root.passwordBuffer = "";
@@ -1609,6 +1613,15 @@ Item {
}
}
Connections {
target: pam
function onUnlockInProgressChanged() {
if (!pam.unlockInProgress && root.unlocking)
root.unlocking = false;
}
}
Binding {
target: pam
property: "buffer"

View File

@@ -22,6 +22,64 @@ Scope {
signal flashMsg
signal unlockRequested
function resetAuthFlows(): void {
passwd.abort();
fprint.abort();
u2f.abort();
errorRetry.running = false;
u2fErrorRetry.running = false;
u2fPendingTimeout.running = false;
passwdActiveTimeout.running = false;
unlockRequestTimeout.running = false;
u2fPending = false;
u2fState = "";
unlockInProgress = false;
}
function recoverFromAuthStall(newState: string): void {
resetAuthFlows();
state = newState;
flashMsg();
stateReset.restart();
fprint.checkAvail();
u2f.checkAvail();
}
function completeUnlock(): void {
if (!unlockInProgress) {
unlockInProgress = true;
passwd.abort();
fprint.abort();
u2f.abort();
errorRetry.running = false;
u2fErrorRetry.running = false;
u2fPendingTimeout.running = false;
u2fPending = false;
u2fState = "";
unlockRequestTimeout.restart();
unlockRequested();
}
}
function proceedAfterPrimaryAuth(): void {
if (SettingsData.enableU2f && SettingsData.u2fMode === "and" && u2f.available) {
u2f.startForSecondFactor();
} else {
completeUnlock();
}
}
function cancelU2fPending(): void {
if (!u2fPending)
return;
u2f.abort();
u2fErrorRetry.running = false;
u2fPendingTimeout.running = false;
u2fPending = false;
u2fState = "";
fprint.checkAvail();
}
FileView {
id: dankshellConfigWatcher
@@ -66,6 +124,13 @@ Scope {
return;
}
unlockRequestTimeout.running = false;
root.unlockInProgress = false;
root.u2fPending = false;
root.u2fState = "";
u2fPendingTimeout.running = false;
u2f.abort();
if (res === PamResult.Error)
root.state = "error";
else if (res === PamResult.MaxTries)
@@ -78,6 +143,18 @@ Scope {
}
}
Connections {
target: passwd
function onActiveChanged() {
if (passwd.active) {
passwdActiveTimeout.restart();
} else {
passwdActiveTimeout.running = false;
}
}
}
PamContext {
id: fprint
@@ -152,6 +229,40 @@ Scope {
onTriggered: fprint.start()
}
Timer {
id: u2fErrorRetry
interval: 800
onTriggered: u2f.start()
}
Timer {
id: u2fPendingTimeout
interval: 30000
onTriggered: root.cancelU2fPending()
}
Timer {
id: passwdActiveTimeout
interval: 15000
onTriggered: {
if (passwd.active)
root.recoverFromAuthStall("error");
}
}
Timer {
id: unlockRequestTimeout
interval: 8000
onTriggered: {
if (root.unlockInProgress)
root.recoverFromAuthStall("error");
}
}
Timer {
id: stateReset
@@ -178,11 +289,9 @@ Scope {
root.state = "";
root.fprintState = "";
root.lockMessage = "";
root.unlockInProgress = false;
root.resetAuthFlows();
} else {
fprint.abort();
passwd.abort();
root.unlockInProgress = false;
root.resetAuthFlows();
}
}
@@ -192,5 +301,21 @@ Scope {
function onEnableFprintChanged(): void {
fprint.checkAvail();
}
function onEnableU2fChanged(): void {
u2f.checkAvail();
}
function onU2fModeChanged(): void {
if (root.lockSecured) {
u2f.abort();
u2fErrorRetry.running = false;
u2fPendingTimeout.running = false;
unlockRequestTimeout.running = false;
root.u2fPending = false;
root.u2fState = "";
u2f.checkAvail();
}
}
}
}