pragma ComponentBehavior: Bound import QtQuick import Quickshell import Quickshell.Wayland import Quickshell.Services.Greetd import qs.HyprChrome.Widgets // greetd greeter: backdrop on every output, login panel on the primary one. // Configured through env set by services/desktop/quickshell-greeter.nix. Scope { id: root readonly property string primaryName: Quickshell.env("QS_GREETER_OUTPUT") ?? "" readonly property string defaultUser: Quickshell.env("QS_GREETER_USER") ?? "" readonly property string sessionCommand: Quickshell.env("QS_GREETER_SESSION") || "start-hyprland" readonly property string sessionName: Quickshell.env("QS_GREETER_SESSION_NAME") || "hyprland" readonly property string hostName: Quickshell.env("QS_GREETER_HOST") || "localhost" readonly property var primaryScreen: { const screens = Quickshell.screens; if (screens.length === 0) return null; for (let i = 0; i < screens.length; i++) { if (screens[i].name === root.primaryName) return screens[i]; } return screens[0]; } // Typed password, held until PAM's first secret prompt arrives. property string pendingSecret: "" // PAM asked a follow-up (OTP etc.) that the user must answer directly. property bool awaitingResponse: false property bool busy: false property bool launching: false function setMessage(text, isError) { content.message = text; content.messageIsError = isError; } function resetToPassword() { root.pendingSecret = ""; root.awaitingResponse = false; root.busy = false; content.inputPrompt = ""; content.responseVisible = false; content.clearSecret(); content.focusSecret(); } function submit(user, secret) { if (!Greetd.available) { root.setMessage("greetd socket unavailable", true); return; } content.failed = false; if (root.awaitingResponse) { root.awaitingResponse = false; root.busy = true; content.clearSecret(); Greetd.respond(secret); return; } if (Greetd.state !== GreetdState.Inactive) return; root.setMessage("", false); root.pendingSecret = secret; root.busy = true; Greetd.createSession(user); } Connections { target: Greetd function onAuthMessage(message, error, responseRequired, echoResponse) { if (!responseRequired) { root.setMessage(message, error); return; } // First hidden prompt is the password already typed. if (root.pendingSecret !== "" && !echoResponse) { const secret = root.pendingSecret; root.pendingSecret = ""; Greetd.respond(secret); return; } root.busy = false; root.awaitingResponse = true; content.inputPrompt = message.replace(/:\s*$/, ""); content.responseVisible = echoResponse; content.clearSecret(); content.focusSecret(); } function onAuthFailure(message) { root.resetToPassword(); content.failed = true; root.setMessage(message || "authentication failed", true); } function onError(error) { root.resetToPassword(); root.setMessage(error, true); } function onReadyToLaunch() { root.setMessage("starting " + root.sessionName, false); root.launching = true; } } // Fade out first: greetd wants the greeter gone promptly after launch(). Timer { running: root.launching interval: 220 onTriggered: Greetd.launch([root.sessionCommand]) } Variants { model: Quickshell.screens ChromeBackdrop { required property var modelData screen: modelData active: true dim: 1 wlrLayer: WlrLayer.Background } } PanelWindow { screen: root.primaryScreen visible: root.primaryScreen !== null WlrLayershell.namespace: "hyprchrome-greeter" WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive exclusionMode: ExclusionMode.Ignore color: "transparent" anchors { top: true left: true right: true bottom: true } LoginContent { id: content width: 640 anchors.horizontalCenter: parent.horizontalCenter y: Math.max(32, Math.round(parent.height * 0.42 - height / 2)) + (1 - opacity) * 12 opacity: root.launching ? 0 : 1 Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } } hostName: root.hostName sessionName: root.sessionName user: root.defaultUser busy: root.busy || root.launching onSubmitted: (user, secret) => root.submit(user, secret) // A different user invalidates a half-finished conversation. onUserEdited: { if (Greetd.state !== GreetdState.Inactive) Greetd.cancelSession(); root.resetToPassword(); content.focusUser(); } onPowerRequested: action => Quickshell.execDetached(["systemctl", action]) Component.onCompleted: { if (root.defaultUser !== "") content.focusSecret(); else content.focusUser(); } } Timer { running: true repeat: true interval: 1000 triggeredOnStart: true onTriggered: content.now = new Date() } } }