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