Files
darmanandClaude Opus 5 f3dddc9150 feat(terra): replace tuigreet with a quickshell greeter
greetd now runs a throwaway Hyprland hosting dotfiles/quickshell/greeter.qml,
configured per host via homelab.greeter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 23:34:10 +02:00

201 lines
5.9 KiB
QML

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()
}
}
}