pragma Singleton import Quickshell import Quickshell.Services.Greetd import Quickshell.Io import QtQuick import QtQuick.Controls import QtQuick.Layouts import QtQuick.Effects ShellRoot { id: root // ── Theme (synced by noctalia-greeter-sync service) ────────────────────── FileView { id: colorsFile path: "/var/lib/noctalia-greeter-theme/colors.json" blockLoading: true } readonly property var nc: { try { return JSON.parse(colorsFile.text()) } catch (e) { return { mPrimary: "#76daa2", mOnPrimary: "#003920", mSurface: "#111412", mOnSurface: "#e1e3de", mSurfaceVariant: "#1d201e", mOnSurfaceVariant: "#c0c9c0", mOutline: "#404942", mShadow: "#000000", mError: "#ffb4ab", mOnError: "#690005", mTertiary: "#a3cddc", mOnTertiary: "#043541" } } } readonly property string wallpaperPath: "/var/lib/noctalia-greeter-theme/wallpaper.jpg" // ── Auth state ──────────────────────────────────────────────────────────── property string password: "" property string statusMsg: "" property bool statusErr: false property bool waitingForPw: false property bool launching: false Component.onCompleted: { Greetd.createSession("moze") } Connections { target: Greetd function onAuthMessage(message, isError, responseRequired, echoResponse) { if (responseRequired) { root.waitingForPw = true if (!root.statusErr) root.statusMsg = "" } else { root.statusMsg = message || "" root.statusErr = false } } function onReadyToLaunch() { root.launching = true root.statusMsg = "Welcome back!" root.statusErr = false Greetd.launch( ["/usr/sbin/dbus-run-session", "/usr/sbin/niri", "--session"], [], true ) } function onAuthFailure(message) { root.statusMsg = message || "Authentication failed" root.statusErr = true root.password = "" root.waitingForPw = false restartTimer.start() } function onError(error) { root.statusMsg = "Session error: " + error root.statusErr = true restartTimer.start() } } Timer { id: restartTimer interval: 2000 repeat: false onTriggered: { root.statusMsg = "" root.statusErr = false Greetd.createSession("moze") } } function submitPassword() { if (!root.waitingForPw || root.password.length === 0) return Greetd.respond(root.password) root.password = "" root.waitingForPw = false root.statusMsg = "Verifying…" root.statusErr = false } // ── Clocks ──────────────────────────────────────────────────────────────── property date clockNow: new Date() property date clockDate: new Date() Timer { interval: 1000; running: true; repeat: true; onTriggered: root.clockNow = new Date() } Timer { interval: 60000; running: true; repeat: true; onTriggered: root.clockDate = new Date() } // ── Greeter window ──────────────────────────────────────────────────────── Variants { model: Quickshell.screens PanelWindow { required property var modelData screen: modelData exclusiveZone: -1 anchors { top: true; bottom: true; left: true; right: true } color: "transparent" keyboardFocus: KeyboardFocus.Exclusive // ── Background ──────────────────────────────────────────────────── Rectangle { anchors.fill: parent color: "#000000" } Image { anchors.fill: parent source: root.wallpaperPath fillMode: Image.PreserveAspectCrop smooth: true mipmap: true } // Gradient overlay (matches noctalia LockScreenBackground) Rectangle { anchors.fill: parent gradient: Gradient { GradientStop { position: 0.0; color: Qt.alpha(root.nc.mShadow, 0.45) } GradientStop { position: 0.3; color: Qt.alpha(root.nc.mShadow, 0.22) } GradientStop { position: 0.7; color: Qt.alpha(root.nc.mShadow, 0.27) } GradientStop { position: 1.0; color: Qt.alpha(root.nc.mShadow, 0.55) } } } // ── Header (mirrors LockScreenHeader) ──────────────────────────── Rectangle { id: header anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.topMargin: 100 width: Math.max(520, headerRow.implicitWidth + 40) height: headerRow.implicitHeight + 32 radius: 14 color: Qt.alpha(root.nc.mSurface, 0.88) border.color: Qt.alpha(root.nc.mOutline, 0.22) border.width: 1 RowLayout { id: headerRow anchors.fill: parent anchors.margins: 18 spacing: 22 // Avatar Rectangle { Layout.preferredWidth: 70 Layout.preferredHeight: 70 Layout.alignment: Qt.AlignVCenter radius: width / 2 color: "transparent" border.color: Qt.alpha(root.nc.mPrimary, 0.85) border.width: 2 // Fallback person icon Text { anchors.centerIn: parent text: "󰀄" font.pixelSize: 32 font.family: "Symbols Nerd Font" color: root.nc.mOnSurfaceVariant } } // Welcome + date ColumnLayout { Layout.alignment: Qt.AlignVCenter spacing: 4 Text { text: "Welcome back, Moze!" font.pixelSize: 20 font.weight: Font.Medium color: root.nc.mOnSurface } Text { text: { const s = Qt.locale().toString(root.clockDate, "dddd, MMMM d") return s.charAt(0).toUpperCase() + s.slice(1) } font.pixelSize: 15 color: root.nc.mOnSurfaceVariant } } Item { Layout.fillWidth: true } // Stacked clock (hh / mm, matches clockStyle: "custom") ColumnLayout { Layout.alignment: Qt.AlignVCenter spacing: -6 Repeater { model: Qt.locale().toString(root.clockNow, "hh\nmm").split("\n") Text { required property string modelData text: modelData font.pixelSize: 32 font.weight: Font.Bold color: root.nc.mOnSurface Layout.alignment: Qt.AlignHCenter } } } } } // ── Password panel ──────────────────────────────────────────────── Column { anchors.horizontalCenter: parent.horizontalCenter anchors.top: header.bottom anchors.topMargin: parent.height * 0.10 spacing: 10 Rectangle { width: 380 height: 64 radius: 14 color: Qt.alpha(root.nc.mSurfaceVariant, 0.92) border.color: { if (root.statusErr) return Qt.alpha(root.nc.mError, 0.75) if (root.waitingForPw) return Qt.alpha(root.nc.mPrimary, 0.85) return Qt.alpha(root.nc.mOutline, 0.35) } border.width: 1.5 Behavior on border.color { ColorAnimation { duration: 180 } } RowLayout { anchors.fill: parent anchors.leftMargin: 18 anchors.rightMargin: 18 spacing: 12 Text { text: root.statusErr ? "󰅙" : (root.waitingForPw ? "󰌋" : "󱗃") font.pixelSize: 18 font.family: "Symbols Nerd Font" color: root.statusErr ? root.nc.mError : root.nc.mOnSurfaceVariant } Text { Layout.fillWidth: true text: { if (root.launching) return "Logging in…" if (root.password.length) return "●".repeat(root.password.length) if (root.waitingForPw) return "Enter password…" return "Please wait…" } font.pixelSize: root.password.length ? 22 : 15 letterSpacing: root.password.length ? 6 : 0 color: root.password.length ? root.nc.mOnSurface : root.nc.mOnSurfaceVariant elide: Text.ElideRight Behavior on font.pixelSize { NumberAnimation { duration: 80 } } } // Blinking cursor when idle waiting for input Rectangle { width: 2; height: 28 color: root.nc.mPrimary visible: root.waitingForPw && root.password.length === 0 property real op: 1.0 opacity: op SequentialAnimation on op { running: root.waitingForPw && root.password.length === 0 loops: Animation.Infinite NumberAnimation { to: 0; duration: 500; easing.type: Easing.InOutQuad } NumberAnimation { to: 1; duration: 500; easing.type: Easing.InOutQuad } } } } } // Status / error pill Rectangle { width: 380 height: 38 radius: 10 visible: root.statusMsg !== "" opacity: visible ? 1.0 : 0.0 Behavior on opacity { NumberAnimation { duration: 200 } } color: root.statusErr ? Qt.alpha(root.nc.mError, 0.13) : Qt.alpha(root.nc.mTertiary, 0.13) border.color: root.statusErr ? Qt.alpha(root.nc.mError, 0.4) : Qt.alpha(root.nc.mTertiary, 0.35) border.width: 1 Text { anchors.centerIn: parent width: parent.width - 24 text: root.statusMsg font.pixelSize: 13 color: root.statusErr ? root.nc.mError : root.nc.mOnSurface horizontalAlignment: Text.AlignHCenter elide: Text.ElideRight } } } // ── Keyboard capture (invisible, full-screen) ───────────────────── TextInput { id: pwInput anchors.fill: parent opacity: 0 echoMode: TextInput.Password passwordMaskDelay: 0 enabled: !root.launching onTextChanged: { if (root.password !== text) root.password = text } Connections { target: root function onPasswordChanged() { if (pwInput.text !== root.password) pwInput.text = root.password } } Keys.onPressed: function(event) { if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { root.submitPassword() event.accepted = true } else if (event.key === Qt.Key_Escape) { root.password = "" event.accepted = true } } Component.onCompleted: forceActiveFocus() } MouseArea { anchors.fill: parent acceptedButtons: Qt.AllButtons onClicked: pwInput.forceActiveFocus() z: -1 } } } }