Files

1509 lines
59 KiB
QML

import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Qt.labs.folderlistmodel
import Quickshell
import Quickshell.Io
import Quickshell.Services.Greetd
import qs.Common
import qs.Modules.Lock
import qs.Services
import qs.DCommon.Widgets
import qs.Widgets
import qs.DCommon.Session
import "../../Common/PamStack.js" as PamStack
Item {
id: root
function encodeFileUrl(path) {
if (!path)
return "";
return "file://" + path.split('/').map(s => encodeURIComponent(s)).join('/');
}
function desktopIdFromPath(path) {
if (!path)
return "";
const parts = path.split("/");
const id = parts.length > 0 ? parts[parts.length - 1] : path;
return id || "";
}
readonly property string xdgDataDirs: Quickshell.env("XDG_DATA_DIRS")
property string screenName: ""
property bool isPrimaryScreen: !Quickshell.screens?.length || screenName === Quickshell.screens[0]?.name
signal launchRequested
property bool weatherInitialized: false
property bool awaitingExternalAuth: false
property bool pendingPasswordResponse: false
property bool passwordSubmitRequested: false
property bool cancelingExternalAuthForPassword: false
property int defaultAuthTimeoutMs: 10000
property int externalAuthTimeoutMs: 30000
property int memoryFlushDelayMs: 120
property string pendingLaunchCommand: ""
property var pendingLaunchEnv: []
property int passwordFailureCount: 0
property int passwordAttemptLimitHint: 0
property string authFeedbackMessage: ""
property string authSuccessMessage: ""
property string greetdPamText: ""
property string systemAuthPamText: ""
property string commonAuthPamText: ""
property string passwordAuthPamText: ""
property string systemLoginPamText: ""
property string systemLocalLoginPamText: ""
property string commonAuthPcPamText: ""
property string loginPamText: ""
property string faillockConfigText: ""
property string externalAuthAutoStartedForUser: ""
property int passwordSessionTransitionRetryCount: 0
property int maxPasswordSessionTransitionRetries: 2
property bool fprintdProbeComplete: false
property bool fprintdHasDevice: false
property bool autoLoginOnSuccess: false
readonly property bool greeterPamStackHasFprint: greeterPamStackHasModule("pam_fprintd")
// Falls back to PAM-only detection until the fprintd D-Bus probe completes.
readonly property bool greeterPamHasFprint: greeterPamStackHasFprint && (!fprintdProbeComplete || fprintdHasDevice)
readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f")
readonly property bool greeterExternalAuthAvailable: (greeterPamHasFprint && SettingsData.greeterEnableFprint) || (greeterPamHasU2f && SettingsData.greeterEnableU2f)
readonly property bool greeterPamHasExternalAuth: greeterPamHasFprint || greeterPamHasU2f
readonly property bool externalAuthInProgress: awaitingExternalAuth || (Greetd.state !== GreetdState.Inactive && passwordSubmitRequested && greeterPamHasExternalAuth && !pendingPasswordResponse)
readonly property string externalAuthStatusMessage: {
if (!externalAuthInProgress)
return "";
if (greeterPamHasFprint && greeterPamHasU2f)
return I18n.tr("Awaiting fingerprint or security key authentication");
if (greeterPamHasFprint)
return I18n.tr("Awaiting fingerprint authentication");
return I18n.tr("Awaiting security key authentication");
}
readonly property string authDisplayMessage: authFeedbackMessage || authSuccessMessage || externalAuthStatusMessage
readonly property bool autoLoginAvailable: GreetdSettings.rememberLastUser && GreetdSettings.rememberLastSession
readonly property bool multipleUsersAvailable: GreeterUsersService.loaded && GreeterUsersService.users.length > 1
// Single-user systems get the picker too when auto-login is available, so the
// auto-login toggle lives inside the dropdown instead of floating on its own.
readonly property bool pickerAvailable: multipleUsersAvailable || (GreeterUsersService.loaded && GreeterUsersService.users.length === 1 && autoLoginAvailable)
readonly property bool showUserPicker: pickerAvailable && !GreeterState.showPasswordInput && !manualUsernameEntry
readonly property bool showAccountSwitchLink: pickerAvailable && manualUsernameEntry && !GreeterState.showPasswordInput && !GreeterState.unlocking
readonly property int userPickerMaxHeight: Math.min(400, Math.max(120, height * 0.35))
property bool userListOpen: false
property bool manualUsernameEntry: false
property bool skipAutoSelectUser: false
property string pickerThemeUsername: ""
function initWeatherService() {
if (weatherInitialized)
return;
if (!GreetdSettings.settingsLoaded)
return;
if (!SettingsData.lockScreenShowWeather)
return;
weatherInitialized = true;
WeatherService.addRef();
WeatherService.forceRefresh();
}
function greeterPamStackHasModule(moduleName) {
const includedPamStacks = [["system-auth", systemAuthPamText], ["common-auth", commonAuthPamText], ["password-auth", passwordAuthPamText], ["system-login", systemLoginPamText], ["system-local-login", systemLocalLoginPamText], ["common-auth-pc", commonAuthPcPamText], ["login", loginPamText]];
return PamStack.stackHasModule(greetdPamText, includedPamStacks, moduleName);
}
function refreshPasswordAttemptPolicyHint() {
const pamSources = [greetdPamText, systemAuthPamText, commonAuthPamText, passwordAuthPamText, systemLoginPamText, systemLocalLoginPamText, commonAuthPcPamText, loginPamText];
passwordAttemptLimitHint = PamStack.attemptLimitHint(pamSources, faillockConfigText);
}
function currentAuthMessage() {
if (GreeterState.pamState === "error")
return I18n.tr("Authentication error - try again");
if (GreeterState.pamState === "max")
return I18n.tr("Too many failed attempts - account may be locked");
if (GreeterState.pamState === "fail") {
if (passwordAttemptLimitHint > 0) {
const attempt = Math.max(1, Math.min(passwordFailureCount, passwordAttemptLimitHint));
const remaining = Math.max(passwordAttemptLimitHint - attempt, 0);
if (remaining > 0) {
return I18n.tr("Authentication failed - attempt %1 of %2").arg(attempt).arg(passwordAttemptLimitHint);
}
return I18n.tr("Authentication failed - lockout can occur");
}
return I18n.tr("Authentication failed - try again");
}
return "";
}
function clearAuthFeedback() {
GreeterState.pamState = "";
authFeedbackMessage = "";
authSuccessMessage = "";
}
function resetPasswordSessionTransition(clearSubmitRequest) {
cancelingExternalAuthForPassword = false;
passwordSessionTransitionRetryCount = 0;
if (clearSubmitRequest)
passwordSubmitRequested = false;
}
Connections {
target: GreetdSettings
function onSettingsLoadedChanged() {
if (GreetdSettings.settingsLoaded) {
initWeatherService();
if (isPrimaryScreen) {
applyLastSuccessfulUser();
finalizeSessionSelection();
}
}
}
function onRememberLastUserChanged() {
if (!isPrimaryScreen)
return;
if (!GreetdSettings.rememberLastUser && GreetdMemory.lastSuccessfulUser) {
GreetdMemory.setLastSuccessfulUser("");
}
applyLastSuccessfulUser();
}
function onRememberLastSessionChanged() {
if (!isPrimaryScreen)
return;
if (!GreetdSettings.rememberLastSession && (GreetdMemory.lastSessionId || GreetdMemory.lastSessionDesktopId || GreetdMemory.lastSessionExec)) {
GreetdMemory.setLastSession("", "");
}
finalizeSessionSelection();
}
}
FileView {
id: greetdPamWatcher
path: "/etc/pam.d/greetd"
printErrors: false
onLoaded: {
root.greetdPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.greetdPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: systemAuthPamWatcher
path: "/etc/pam.d/system-auth"
printErrors: false
onLoaded: {
root.systemAuthPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.systemAuthPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: commonAuthPamWatcher
path: "/etc/pam.d/common-auth"
printErrors: false
onLoaded: {
root.commonAuthPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.commonAuthPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: passwordAuthPamWatcher
path: "/etc/pam.d/password-auth"
printErrors: false
onLoaded: {
root.passwordAuthPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.passwordAuthPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: systemLoginPamWatcher
path: "/etc/pam.d/system-login"
printErrors: false
onLoaded: {
root.systemLoginPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.systemLoginPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: systemLocalLoginPamWatcher
path: "/etc/pam.d/system-local-login"
printErrors: false
onLoaded: {
root.systemLocalLoginPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.systemLocalLoginPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: commonAuthPcPamWatcher
path: "/etc/pam.d/common-auth-pc"
printErrors: false
onLoaded: {
root.commonAuthPcPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.commonAuthPcPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: loginPamWatcher
path: "/etc/pam.d/login"
printErrors: false
onLoaded: {
root.loginPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.loginPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: faillockConfigWatcher
path: "/etc/security/faillock.conf"
printErrors: false
onLoaded: {
root.faillockConfigText = text();
root.refreshPasswordAttemptPolicyHint();
}
onLoadFailed: {
root.faillockConfigText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
Component.onCompleted: {
initWeatherService();
refreshPasswordAttemptPolicyHint();
if (isPrimaryScreen)
applyLastSuccessfulUser();
fprintdDeviceProbe.running = true;
}
function applyPickerPreviewTheme() {
let previewUser = (pickerThemeUsername || "").trim();
if (!previewUser && GreetdSettings.rememberLastUser)
previewUser = (GreetdMemory.lastSuccessfulUser || "").trim();
if (previewUser)
GreeterUserTheme.applyForUser(previewUser);
else
GreeterUserTheme.applyDefault();
}
function applyLastSuccessfulUser() {
if (root.skipAutoSelectUser)
return;
if (!GreetdSettings.settingsLoaded || !GreetdSettings.rememberLastUser)
return;
const lastUser = GreetdMemory.lastSuccessfulUser;
if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) {
selectUser(lastUser, true);
}
}
function enterManualUsernameEntry() {
if (!root.pickerAvailable || GreeterState.showPasswordInput)
return;
root.manualUsernameEntry = true;
root.userListOpen = false;
GreeterState.username = "";
GreeterState.usernameInput = "";
GreeterState.selectedUserIndex = -1;
root.applyPickerPreviewTheme();
Qt.callLater(() => inputField.forceActiveFocus());
}
function returnToUserListFromManualEntry() {
if (!root.pickerAvailable)
return;
root.manualUsernameEntry = false;
root.userListOpen = true;
GreeterState.username = "";
GreeterState.usernameInput = "";
root.applyPickerPreviewTheme();
}
function returnToUserPicker() {
if (!root.pickerAvailable || GreeterState.unlocking)
return;
root.manualUsernameEntry = false;
root.skipAutoSelectUser = true;
awaitingExternalAuth = false;
pendingPasswordResponse = false;
passwordSubmitRequested = false;
resetPasswordSessionTransition(true);
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
clearAuthFeedback();
passwordFailureCount = 0;
externalAuthAutoStartedForUser = "";
if (Greetd.state !== GreetdState.Inactive)
Greetd.cancelSession();
const previousUser = GreeterState.username;
GreeterState.reset();
inputField.passwordVisible = false;
PortalService.profileImage = "";
if (previousUser)
root.pickerThemeUsername = previousUser;
root.applyPickerPreviewTheme();
root.userListOpen = true;
}
function selectUser(rawValue, skipDropdownUpdate) {
const user = (rawValue || "").trim();
if (!user)
return;
root.manualUsernameEntry = false;
root.skipAutoSelectUser = false;
submitUsername(user, skipDropdownUpdate === true);
}
function submitUsername(rawValue, skipDropdownUpdate) {
const user = (rawValue || "").trim();
if (!user)
return;
if (GreeterState.username !== user) {
passwordFailureCount = 0;
clearAuthFeedback();
externalAuthAutoStartedForUser = "";
}
root.pickerThemeUsername = user;
GreeterState.username = user;
GreeterState.usernameInput = user;
GreeterState.showPasswordInput = true;
if (!skipDropdownUpdate && typeof GreeterUsersService !== "undefined") {
const idx = GreeterUsersService.usernames.indexOf(user);
GreeterState.selectedUserIndex = idx;
}
root.userListOpen = false;
PortalService.getGreeterUserProfileImage(user);
GreeterState.passwordBuffer = "";
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
maybeAutoStartExternalAuth();
}
function submitBufferedPassword() {
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
awaitingExternalAuth = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.restart();
// Some PAM stacks expect an explicit empty response to advance U2F/fprint or fail normally.
Greetd.respond(GreeterState.passwordBuffer || "");
GreeterState.passwordBuffer = "";
return true;
}
function requestPasswordSessionTransition() {
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
if (!passwordSubmitRequested && !hasPasswordBuffer)
return;
if (cancelingExternalAuthForPassword)
return;
if (passwordSessionTransitionRetryCount >= maxPasswordSessionTransitionRetries) {
pendingPasswordResponse = false;
awaitingExternalAuth = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
resetPasswordSessionTransition(true);
GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
placeholderDelay.restart();
Greetd.cancelSession();
return;
}
cancelingExternalAuthForPassword = true;
passwordSessionTransitionRetryCount = passwordSessionTransitionRetryCount + 1;
awaitingExternalAuth = false;
pendingPasswordResponse = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
Greetd.cancelSession();
}
function startAuthSession(submitPassword) {
submitPassword = submitPassword === true;
if (!GreeterState.showPasswordInput || !GreeterState.username)
return;
if (GreeterState.unlocking)
return;
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
if (Greetd.state !== GreetdState.Inactive) {
if (pendingPasswordResponse && submitPassword)
submitBufferedPassword();
else if (submitPassword)
passwordSubmitRequested = true;
return;
}
if (cancelingExternalAuthForPassword) {
if (submitPassword)
passwordSubmitRequested = true;
return;
}
if (!submitPassword && !hasPasswordBuffer && !root.greeterExternalAuthAvailable)
return;
pendingPasswordResponse = false;
passwordSubmitRequested = submitPassword;
awaitingExternalAuth = !submitPassword && !hasPasswordBuffer && root.greeterExternalAuthAvailable;
// Let the effective PAM stack finish external authentication.
const waitingOnPamExternalBeforePassword = submitPassword && root.greeterPamHasExternalAuth;
authTimeout.interval = (awaitingExternalAuth || waitingOnPamExternalBeforePassword) ? externalAuthTimeoutMs : defaultAuthTimeoutMs;
authTimeout.restart();
Greetd.createSession(GreeterState.username);
}
function maybeAutoStartExternalAuth() {
if (!GreeterState.showPasswordInput || !GreeterState.username)
return;
if (!root.greeterExternalAuthAvailable)
return;
if (GreeterState.unlocking || Greetd.state !== GreetdState.Inactive)
return;
if (passwordSubmitRequested || cancelingExternalAuthForPassword)
return;
if (GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0)
return;
if (externalAuthAutoStartedForUser === GreeterState.username)
return;
externalAuthAutoStartedForUser = GreeterState.username;
startAuthSession(false);
}
function isExternalAuthPrompt(message, responseRequired) {
// Non-response PAM messages commonly represent waiting states (fprint/U2F/token touch).
return !responseRequired;
}
Component.onDestruction: {
if (weatherInitialized)
WeatherService.removeRef();
}
Process {
id: greeterAutoLoginPendingProcess
command: ["sh", "-c", "mkdir -p $(dirname " + JSON.stringify((Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter") + "/.local/state/auto-login-sync-pending") + ") && touch " + JSON.stringify((Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter") + "/.local/state/auto-login-sync-pending")]
running: false
}
// 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: GreetdMemory
enabled: isPrimaryScreen
function onLastSuccessfulUserChanged() {
applyLastSuccessfulUser();
}
function onMemoryReadyChanged() {
finalizeSessionSelection();
}
}
Connections {
target: GreeterUsersService
function onLoadedChanged() {
if (GreeterUsersService.loaded && isPrimaryScreen)
applyPickerPreviewTheme();
}
function onSyncedThemePathsChanged() {
if (!isPrimaryScreen)
return;
if (GreeterState.username)
GreeterUserTheme.applyForUser(GreeterState.username);
else if (root.showUserPicker || root.userListOpen)
applyPickerPreviewTheme();
}
}
Connections {
target: GreeterState
function onUsernameChanged() {
if (GreeterState.username) {
root.pickerThemeUsername = GreeterState.username;
GreeterUserTheme.applyForUser(GreeterState.username);
PortalService.getGreeterUserProfileImage(GreeterState.username);
} else if (root.showUserPicker || root.userListOpen) {
applyPickerPreviewTheme();
}
}
function onShowPasswordInputChanged() {
if (GreeterState.showPasswordInput)
root.userListOpen = false;
}
}
onShowUserPickerChanged: {
if (showUserPicker && !GreeterState.username)
applyPickerPreviewTheme();
if (!showUserPicker)
userListOpen = false;
}
Rectangle {
anchors.fill: parent
color: SettingsData.effectiveWallpaperBackgroundColor
}
readonly property bool hasCustomWallpaper: SettingsData.lockScreenWallpaperPath !== ""
readonly property string wallpaperSource: {
if (hasCustomWallpaper)
return encodeFileUrl(SettingsData.lockScreenWallpaperPath);
var w = SessionData.getMonitorWallpaper(screenName);
return (w && !w.startsWith("#")) ? encodeFileUrl(w) : "";
}
readonly property string wallpaperFillModeName: {
if (SettingsData.lockScreenWallpaperFillMode !== "")
return SettingsData.lockScreenWallpaperFillMode;
return hasCustomWallpaper ? "Fill" : SessionData.getMonitorWallpaperFillMode(screenName);
}
DBackdrop {
anchors.fill: parent
screenName: root.screenName
blur: Theme.lockScreenBlur
blurMax: Theme.lockScreenBlurMax
visible: root.wallpaperSource === "" || wallpaperBackground.status === Image.Error
}
Image {
id: wallpaperBackground
anchors.fill: parent
source: root.wallpaperSource
fillMode: Theme.getFillMode(root.wallpaperFillModeName)
smooth: true
asynchronous: false
cache: true
visible: source !== ""
layer.enabled: true
layer.effect: MultiEffect {
autoPaddingEnabled: false
blurEnabled: true
blur: 0.8
blurMax: 32
blurMultiplier: 1
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
anchors.fill: parent
color: "black"
opacity: 0.4
}
SystemClock {
id: systemClock
precision: SystemClock.Seconds
}
Rectangle {
anchors.fill: parent
color: "transparent"
MouseArea {
anchors.fill: parent
enabled: root.userListOpen
visible: root.userListOpen
onClicked: root.userListOpen = false
}
Column {
id: greeterMainColumn
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
width: 380
Item {
id: clockContainer
width: parent.width
height: clockText.implicitHeight
Row {
id: clockText
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
spacing: 0
property string fullTimeStr: {
const format = SettingsData.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: Theme.fontWeight
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: Theme.fontWeight
color: "white"
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: ":"
font.pixelSize: 120
font.weight: Theme.fontWeight
color: "white"
}
StyledText {
width: 75
text: clockText.minutes.length > 0 ? clockText.minutes[0] : ""
font.pixelSize: 120
font.weight: Theme.fontWeight
color: "white"
horizontalAlignment: Text.AlignHCenter
}
StyledText {
width: 75
text: clockText.minutes.length > 1 ? clockText.minutes[1] : ""
font.pixelSize: 120
font.weight: Theme.fontWeight
color: "white"
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: clockText.hasSeconds ? ":" : ""
font.pixelSize: 120
font.weight: Theme.fontWeight
color: "white"
visible: clockText.hasSeconds
}
StyledText {
width: 75
text: clockText.hasSeconds && clockText.seconds.length > 0 ? clockText.seconds[0] : ""
font.pixelSize: 120
font.weight: Theme.fontWeight
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: Theme.fontWeight
color: "white"
horizontalAlignment: Text.AlignHCenter
visible: clockText.hasSeconds
}
StyledText {
width: 20
text: " "
font.pixelSize: 120
font.weight: Theme.fontWeight
color: "white"
visible: clockText.ampm !== ""
}
StyledText {
text: clockText.ampm
font.pixelSize: 120
font.weight: Theme.fontWeight
color: "white"
visible: clockText.ampm !== ""
}
}
}
StyledText {
id: dateText
anchors.horizontalCenter: parent.horizontalCenter
text: systemClock.date.toLocaleDateString(I18n.locale(), SettingsData.lockDateFormat !== "" ? SettingsData.lockDateFormat : Locale.LongFormat)
font.pixelSize: Theme.fontSizeXLarge
color: "white"
opacity: 0.9
}
ColumnLayout {
id: authColumn
width: parent.width
spacing: Theme.spacingM
RowLayout {
spacing: Theme.spacingL
Layout.fillWidth: true
Item {
Layout.preferredWidth: 60
Layout.preferredHeight: 60
visible: SettingsData.lockScreenShowProfileImage || root.pickerAvailable
DCircularImage {
anchors.fill: parent
ringWidth: Theme.avatarRingWidth
ringColor: Theme.avatarRingColor
imageSource: {
const displayUser = GreeterState.username || root.pickerThemeUsername;
if (displayUser) {
const cachedPath = GreeterUsersService.profileImagePath(displayUser);
if (cachedPath)
return encodeFileUrl(cachedPath);
}
if (PortalService.profileImage === "")
return "";
if (PortalService.profileImage.startsWith("/"))
return encodeFileUrl(PortalService.profileImage);
return PortalService.profileImage;
}
fallbackIcon: "material:person"
}
Rectangle {
anchors.fill: parent
radius: width / 2
color: "transparent"
border.color: Theme.primary
border.width: (avatarPickerArea.containsMouse || root.userListOpen) && !GreeterState.showPasswordInput ? 2 : 0
visible: root.pickerAvailable
Behavior on border.width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Switch-user affordance: hover scrim over the selected user's avatar.
Rectangle {
anchors.fill: parent
radius: width / 2
color: Qt.rgba(0, 0, 0, 0.55)
opacity: (root.pickerAvailable && GreeterState.showPasswordInput && avatarPickerArea.containsMouse) ? 1 : 0
visible: opacity > 0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
DIcon {
anchors.centerIn: parent
name: "switch_account"
size: 24
color: "white"
}
}
MouseArea {
id: avatarPickerArea
anchors.fill: parent
visible: root.pickerAvailable
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (GreeterState.showPasswordInput)
root.returnToUserPicker();
else if (root.manualUsernameEntry)
root.returnToUserListFromManualEntry();
else
root.userListOpen = !root.userListOpen;
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: root.userListOpen ? Math.max(Theme.buttonHeightM, userPicker.implicitHeight + Theme.spacingM * 2) : Theme.buttonHeightM
visible: root.showUserPicker
clip: true
radius: Theme.cornerRadiusXL
color: Theme.surfaceContainerHigh
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
GreeterUserPicker {
id: userPicker
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: root.userListOpen ? undefined : parent.verticalCenter
anchors.top: root.userListOpen ? parent.top : undefined
anchors.margins: Theme.spacingM
maxExpandedHeight: root.userPickerMaxHeight
expanded: root.userListOpen
autoLoginVisible: root.autoLoginAvailable
autoLoginChecked: root.autoLoginOnSuccess
manualEntryVisible: true
onUserSelected: username => root.selectUser(username, false)
onToggleRequested: root.userListOpen = !root.userListOpen
onAutoLoginToggled: root.autoLoginOnSuccess = !root.autoLoginOnSuccess
onManualEntryRequested: root.enterManualUsernameEntry()
}
Behavior on Layout.preferredHeight {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
}
DTextField {
id: inputField
readonly property bool inputIdle: (Greetd.state === GreetdState.Inactive || root.awaitingExternalAuth || root.pendingPasswordResponse) && !GreeterState.unlocking
Layout.fillWidth: true
visible: !root.showUserPicker
focus: visible
expressive: true
morph: GreeterState.showPasswordInput
busy: !inputIdle
isError: GreeterState.pamState !== ""
leftIconName: GreeterState.showPasswordInput ? "lock" : "person"
labelText: {
if (GreeterState.unlocking)
return I18n.tr("Logging in...");
if (!inputIdle)
return I18n.tr("Authenticating...");
if (GreeterState.showPasswordInput)
return I18n.tr("Password...");
return I18n.tr("Username...");
}
placeholderColor: inputIdle ? Theme.outline : Theme.primary
showPasswordToggle: GreeterState.showPasswordInput && inputIdle
echoMode: GreeterState.showPasswordInput ? TextInput.Password : TextInput.Normal
showAcceptButton: inputIdle
acceptButtonName: I18n.tr("Login")
text: GreeterState.showPasswordInput ? GreeterState.passwordBuffer : GreeterState.usernameInput
trailingContent: Row {
DActionButton {
iconName: root.greeterPamHasFprint ? "fingerprint" : "key"
tooltipText: root.greeterPamHasFprint ? I18n.tr("Fingerprint") : I18n.tr("Security key")
buttonSize: inputField.accessorySize
visible: GreeterState.showPasswordInput && root.greeterExternalAuthAvailable && GreeterState.passwordBuffer.length === 0 && inputField.inputIdle
onClicked: root.startAuthSession(false)
}
DActionButton {
iconName: "keyboard"
Accessible.name: I18n.tr("Keyboard")
buttonSize: inputField.accessorySize
visible: inputField.inputIdle
onClicked: {
if (keyboard_controller.isKeyboardActive)
keyboard_controller.hide();
else
keyboard_controller.show();
}
}
}
onTextEdited: {
if (!GreeterState.showPasswordInput) {
GreeterState.usernameInput = text;
return;
}
GreeterState.passwordBuffer = text;
if (text.length === 0)
root.passwordSubmitRequested = false;
}
onAccepted: {
if (GreeterState.showPasswordInput) {
root.startAuthSession(true);
return;
}
if (text.trim())
root.submitUsername(text);
}
Component.onCompleted: {
if (isPrimaryScreen && !powerMenu.isVisible)
forceActiveFocus();
}
onVisibleChanged: {
if (visible && isPrimaryScreen && !powerMenu.isVisible)
forceActiveFocus();
}
KeyboardController {
id: keyboard_controller
target: inputField
rootObject: root
expressive: true
}
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: root.showAccountSwitchLink ? 28 : 0
visible: root.showAccountSwitchLink
StyledText {
id: accountSwitchLabel
anchors.horizontalCenter: parent.horizontalCenter
text: I18n.tr("Back to user list", "greeter link to return from manual username entry to user picker")
color: Theme.primary
font.pixelSize: Theme.fontSizeSmall
font.underline: accountSwitchMouse.containsMouse
}
MouseArea {
id: accountSwitchMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.returnToUserListFromManualEntry()
}
}
StyledText {
Layout.fillWidth: true
Layout.preferredHeight: root.authDisplayMessage !== "" ? 38 : 0
Layout.topMargin: -Theme.spacingS
Layout.bottomMargin: -Theme.spacingS
text: root.authDisplayMessage
color: root.authFeedbackMessage !== "" ? Theme.error : (root.authSuccessMessage !== "" ? Theme.success : Theme.surfaceVariantText)
font.pixelSize: Theme.fontSizeSmall
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
maximumLineCount: 2
opacity: root.authDisplayMessage !== "" ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
LockStatusRow {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Theme.spacingXL
showWeather: SettingsData.lockScreenShowWeather
useFahrenheit: SettingsData.useFahrenheit
}
DActionButton {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: Theme.spacingXL
visible: SettingsData.lockScreenShowPowerActions
iconName: "power_settings_new"
Accessible.name: I18n.tr("Power")
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
}
}
DDropdown {
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.sessionManuallySelected = true;
GreeterState.currentSessionIndex = idx;
GreeterState.selectedSession = GreeterState.sessionExecs[idx];
GreeterState.selectedSessionPath = GreeterState.sessionPaths[idx];
GreeterState.selectedSessionDesktopId = GreeterState.sessionDesktopIds[idx];
}
}
}
}
property string currentSessionName: GreeterState.sessionList[GreeterState.currentSessionIndex] || ""
function finalizeSessionSelection() {
if (GreeterState.sessionManuallySelected)
return;
if (GreeterState.sessionList.length === 0)
return;
if (!GreetdMemory.memoryReady)
return;
if (!GreetdSettings.settingsLoaded)
return;
const savedSession = GreetdSettings.rememberLastSession ? GreetdMemory.lastSessionId : "";
const savedDesktopId = GreetdSettings.rememberLastSession ? (GreetdMemory.lastSessionDesktopId || desktopIdFromPath(GreetdMemory.lastSessionId)) : "";
if ((savedSession || savedDesktopId) && GreetdSettings.rememberLastSession) {
for (var i = 0; i < GreeterState.sessionPaths.length; i++) {
if ((savedDesktopId && GreeterState.sessionDesktopIds[i] === savedDesktopId) || (savedSession && GreeterState.sessionPaths[i] === savedSession)) {
GreeterState.currentSessionIndex = i;
GreeterState.selectedSession = GreeterState.sessionExecs[i] || "";
GreeterState.selectedSessionPath = GreeterState.sessionPaths[i];
GreeterState.selectedSessionDesktopId = GreeterState.sessionDesktopIds[i] || "";
return;
}
}
}
GreeterState.currentSessionIndex = 0;
GreeterState.selectedSession = GreeterState.sessionExecs[0] || "";
GreeterState.selectedSessionPath = GreeterState.sessionPaths[0] || "";
GreeterState.selectedSessionDesktopId = GreeterState.sessionDesktopIds[0] || "";
}
property var sessionDirs: {
const homeDir = Quickshell.env("HOME") || "";
const dirs = ["/usr/share/wayland-sessions", "/usr/share/xsessions", "/usr/local/share/wayland-sessions", "/usr/local/share/xsessions"];
if (homeDir) {
dirs.push(homeDir + "/.local/share/wayland-sessions");
dirs.push(homeDir + "/.local/share/xsessions");
}
if (xdgDataDirs) {
xdgDataDirs.split(":").forEach(dir => {
if (dir) {
dirs.push(dir + "/wayland-sessions");
dirs.push(dir + "/xsessions");
}
});
}
// _addSession guards against a session name already existing
// so we have to load from the user directories first so they
// correctly override a system configuration
return dirs.reverse();
}
property var _pendingFiles: ({})
property int _pendingCount: 0
function _addSession(path, name, exec) {
if (!name || !exec || GreeterState.sessionList.includes(name))
return;
GreeterState.sessionList = GreeterState.sessionList.concat([name]);
GreeterState.sessionExecs = GreeterState.sessionExecs.concat([exec]);
GreeterState.sessionPaths = GreeterState.sessionPaths.concat([path]);
GreeterState.sessionDesktopIds = GreeterState.sessionDesktopIds.concat([desktopIdFromPath(path)]);
}
function _parseDesktopFile(content, path) {
let name = "";
let exec = "";
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (!name && line.startsWith("Name="))
name = line.substring(5).trim();
else if (!exec && line.startsWith("Exec="))
exec = line.substring(5).trim();
if (name && exec)
break;
}
_addSession(path, name, exec);
}
function _loadDesktopFile(filePath) {
if (_pendingFiles[filePath])
return;
_pendingFiles[filePath] = true;
_pendingCount++;
const loader = desktopFileLoader.createObject(root, {
"filePath": filePath
});
}
function _onFileLoaded(filePath) {
_pendingCount--;
if (_pendingCount === 0)
Qt.callLater(finalizeSessionSelection);
}
Component {
id: desktopFileLoader
FileView {
id: fv
property string filePath: ""
path: filePath
onLoaded: {
root._parseDesktopFile(text(), filePath);
root._onFileLoaded(filePath);
fv.destroy();
}
onLoadFailed: {
root._onFileLoaded(filePath);
fv.destroy();
}
}
}
Repeater {
model: isPrimaryScreen ? sessionDirs : []
Item {
required property string modelData
FolderListModel {
folder: encodeFileUrl(modelData)
nameFilters: ["*.desktop"]
showDirs: false
showDotAndDotDot: false
onStatusChanged: {
if (status !== FolderListModel.Ready)
return;
for (let i = 0; i < count; i++) {
let fp = get(i, "filePath");
if (fp.startsWith("file://"))
fp = fp.substring(7);
root._loadDesktopFile(fp);
}
}
}
}
}
Connections {
target: Greetd
enabled: isPrimaryScreen
function onAuthMessage(message, error, responseRequired, echoResponse) {
if (responseRequired) {
cancelingExternalAuthForPassword = false;
passwordSessionTransitionRetryCount = 0;
awaitingExternalAuth = false;
pendingPasswordResponse = true;
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
if (!passwordSubmitRequested && hasPasswordBuffer)
passwordSubmitRequested = true;
if (passwordSubmitRequested && !root.submitBufferedPassword())
passwordSubmitRequested = false;
if (passwordSubmitRequested || hasPasswordBuffer) {
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.restart();
} else {
authTimeout.stop();
}
return;
}
pendingPasswordResponse = false;
const externalPrompt = root.isExternalAuthPrompt(message, responseRequired);
if (!passwordSubmitRequested)
awaitingExternalAuth = root.greeterExternalAuthAvailable && externalPrompt;
if (awaitingExternalAuth || (passwordSubmitRequested && externalPrompt && root.greeterPamHasExternalAuth))
authTimeout.interval = externalAuthTimeoutMs;
else
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.restart();
Greetd.respond("");
}
function onStateChanged() {
if (Greetd.state === GreetdState.Inactive) {
const resumePasswordSubmit = cancelingExternalAuthForPassword && passwordSubmitRequested;
awaitingExternalAuth = false;
pendingPasswordResponse = false;
cancelingExternalAuthForPassword = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
if (resumePasswordSubmit) {
Qt.callLater(function () {
root.startAuthSession(true);
});
return;
}
resetPasswordSessionTransition(true);
}
}
function onReadyToLaunch() {
awaitingExternalAuth = false;
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
passwordFailureCount = 0;
clearAuthFeedback();
authSuccessMessage = I18n.tr("Authenticated!");
const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex];
const sessionPath = GreeterState.selectedSessionPath || GreeterState.sessionPaths[GreeterState.currentSessionIndex];
const sessionDesktopId = GreeterState.selectedSessionDesktopId || GreeterState.sessionDesktopIds[GreeterState.currentSessionIndex] || desktopIdFromPath(sessionPath);
if (!sessionCmd) {
GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
placeholderDelay.restart();
return;
}
GreeterState.unlocking = true;
launchTimeout.restart();
if (GreetdSettings.rememberLastSession) {
GreetdMemory.setLastSession(sessionPath, sessionDesktopId);
} else if (GreetdMemory.lastSessionId || GreetdMemory.lastSessionDesktopId || GreetdMemory.lastSessionExec) {
GreetdMemory.setLastSession("", "");
}
if (GreetdSettings.rememberLastUser) {
GreetdMemory.setLastSuccessfulUser(GreeterState.username);
} else if (GreetdMemory.lastSuccessfulUser) {
GreetdMemory.setLastSuccessfulUser("");
}
if (root.autoLoginOnSuccess)
greeterAutoLoginPendingProcess.running = true;
pendingLaunchCommand = sessionCmd;
pendingLaunchEnv = ["XDG_SESSION_TYPE=wayland"];
if (Quickshell.env("DMS_VOID") === "1")
pendingLaunchEnv.push("LIBSEAT_BACKEND=logind");
memoryFlushTimer.restart();
}
function onAuthFailure(message) {
awaitingExternalAuth = false;
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
launchTimeout.stop();
GreeterState.unlocking = false;
if (PamStack.isLikelyLockoutMessage(message)) {
GreeterState.pamState = "max";
} else {
GreeterState.pamState = "fail";
passwordFailureCount = passwordFailureCount + 1;
}
authFeedbackMessage = currentAuthMessage();
inputField.shake();
GreeterState.passwordBuffer = "";
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();
inputField.shake();
GreeterState.passwordBuffer = "";
placeholderDelay.restart();
Greetd.cancelSession();
}
}
Timer {
id: memoryFlushTimer
interval: memoryFlushDelayMs
onTriggered: {
if (!pendingLaunchCommand)
return;
const sessionCommand = pendingLaunchCommand;
const launchEnv = pendingLaunchEnv;
pendingLaunchCommand = "";
pendingLaunchEnv = [];
const sessionArgs = sessionCommand.trim().split(/\s+/);
const needsVoidDbusSession = Quickshell.env("DMS_VOID") === "1" && !Quickshell.env("DBUS_SESSION_BUS_ADDRESS") && sessionArgs[0] !== "dbus-run-session";
const launchArgs = needsVoidDbusSession ? ["dbus-run-session"].concat(sessionArgs) : sessionArgs;
Greetd.launch(launchArgs, launchEnv);
}
}
Timer {
id: authTimeout
interval: defaultAuthTimeoutMs
onTriggered: {
if (GreeterState.unlocking || Greetd.state === GreetdState.Inactive)
return;
awaitingExternalAuth = false;
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
authTimeout.interval = defaultAuthTimeoutMs;
GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
GreeterState.passwordBuffer = "";
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: SettingsData.powerActionConfirm
powerActionHoldDurationOverride: SettingsData.powerActionHoldDuration
powerMenuActionsOverride: SettingsData.powerMenuActions
powerMenuDefaultActionOverride: SettingsData.powerMenuDefaultAction
powerMenuGridLayoutOverride: SettingsData.powerMenuGridLayout
requiredActions: ["poweroff"]
onClosed: {
if (isPrimaryScreen && inputField && inputField.forceActiveFocus) {
Qt.callLater(() => inputField.forceActiveFocus());
}
}
}
}