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>
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user