summaryrefslogtreecommitdiff
path: root/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Greetd/GreeterContent.qml
diff options
context:
space:
mode:
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.qml1908
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());
+ }
+ }
+ }
+}