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>
305 lines
9.5 KiB
QML
305 lines
9.5 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import qs.HyprChrome.Theme
|
|
|
|
// Headless visual core of the login greeter; Greeter.qml owns greetd.
|
|
Item {
|
|
id: root
|
|
|
|
property string hostName: ""
|
|
property string sessionName: ""
|
|
property date now: new Date()
|
|
|
|
// Label over the secret field. Empty means the plain password step; set
|
|
// when PAM asks for something else (OTP, PIN).
|
|
property string inputPrompt: ""
|
|
property bool responseVisible: false
|
|
|
|
property string message: ""
|
|
property bool messageIsError: false
|
|
property bool failed: false
|
|
property bool busy: false
|
|
|
|
property alias user: userInput.text
|
|
property alias response: secretInput.text
|
|
|
|
signal submitted(string user, string response)
|
|
signal userEdited
|
|
signal powerRequested(string action)
|
|
|
|
function focusSecret() { secretInput.forceActiveFocus(); }
|
|
function focusUser() { userInput.forceActiveFocus(); }
|
|
function clearSecret() { secretInput.text = ""; }
|
|
|
|
implicitWidth: 640
|
|
implicitHeight: panel.implicitHeight
|
|
|
|
function submit() {
|
|
if (root.busy)
|
|
return;
|
|
if (userInput.text.trim() === "") {
|
|
root.focusUser();
|
|
return;
|
|
}
|
|
root.submitted(userInput.text.trim(), secretInput.text);
|
|
}
|
|
|
|
component MicroText: Text {
|
|
color: Theme.muted
|
|
font.family: Theme.microFont
|
|
font.pixelSize: 8
|
|
font.letterSpacing: 0.9
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
component Field: Rectangle {
|
|
id: field
|
|
|
|
property bool active: false
|
|
property bool alert: false
|
|
property string glyph: ""
|
|
default property alias input: slot.data
|
|
|
|
Layout.fillWidth: true
|
|
implicitHeight: 36
|
|
color: Theme.selection
|
|
border.width: 1
|
|
border.color: field.alert ? Theme.hot : field.active ? Theme.accentAlpha(0.7) : Theme.hair
|
|
|
|
Behavior on border.color { ColorAnimation { duration: 120 } }
|
|
|
|
// Focus tick on the left edge.
|
|
Rectangle {
|
|
width: 2
|
|
height: parent.height
|
|
color: Theme.accent
|
|
visible: field.active
|
|
}
|
|
|
|
Text {
|
|
id: glyphText
|
|
|
|
x: 12
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: field.glyph
|
|
color: field.active ? Theme.accent : Theme.disabled
|
|
font.family: Theme.displayFont
|
|
font.pixelSize: 14
|
|
font.bold: true
|
|
}
|
|
|
|
Item {
|
|
id: slot
|
|
|
|
anchors.left: glyphText.right
|
|
anchors.leftMargin: 10
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 12
|
|
anchors.top: parent.top
|
|
anchors.bottom: parent.bottom
|
|
}
|
|
}
|
|
|
|
component PowerChip: Rectangle {
|
|
id: chip
|
|
|
|
property string label: ""
|
|
property string action: ""
|
|
|
|
width: chipLabel.implicitWidth + 16
|
|
height: chipLabel.implicitHeight + 8
|
|
color: chipMouse.containsMouse ? Theme.accent : "transparent"
|
|
border.width: 1
|
|
border.color: chipMouse.containsMouse ? Theme.accent : Theme.disabled
|
|
|
|
Text {
|
|
id: chipLabel
|
|
|
|
anchors.centerIn: parent
|
|
text: chip.label
|
|
color: chipMouse.containsMouse ? Theme.surface : Theme.muted
|
|
font.family: Theme.microFont
|
|
font.pixelSize: 9
|
|
font.letterSpacing: 0.8
|
|
}
|
|
|
|
MouseArea {
|
|
id: chipMouse
|
|
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: root.powerRequested(chip.action)
|
|
}
|
|
}
|
|
|
|
LoginPanel {
|
|
id: panel
|
|
|
|
width: root.width
|
|
panelId: "LGN"
|
|
title: "SESSION // " + root.hostName.toUpperCase()
|
|
meta: "GREETD"
|
|
busy: root.busy
|
|
|
|
ColumnLayout {
|
|
width: parent.width
|
|
spacing: 16
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 20
|
|
|
|
// ---- clock column ----
|
|
ColumnLayout {
|
|
Layout.preferredWidth: 190
|
|
Layout.alignment: Qt.AlignTop
|
|
spacing: 4
|
|
|
|
Text {
|
|
text: Qt.formatTime(root.now, "HH:mm")
|
|
color: Theme.text
|
|
font.family: Theme.displayFont
|
|
font.pixelSize: 54
|
|
font.letterSpacing: 2
|
|
}
|
|
|
|
MicroText {
|
|
text: Qt.formatDate(root.now, "ddd dd.MM.yyyy").toUpperCase()
|
|
color: Theme.accent
|
|
font.pixelSize: 10
|
|
}
|
|
|
|
// Seconds as a segment meter: one cell per five seconds.
|
|
Row {
|
|
Layout.topMargin: 10
|
|
spacing: 3
|
|
|
|
Repeater {
|
|
model: 12
|
|
|
|
Rectangle {
|
|
required property int index
|
|
|
|
width: 11
|
|
height: 5
|
|
color: index < Math.floor(root.now.getSeconds() / 5) + 1
|
|
? Theme.accent : Theme.raised
|
|
}
|
|
}
|
|
}
|
|
|
|
MicroText {
|
|
Layout.topMargin: 10
|
|
text: "SESSION " + root.sessionName.toUpperCase()
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
Layout.fillHeight: true
|
|
implicitWidth: 1
|
|
color: Theme.textAlpha(0.12)
|
|
}
|
|
|
|
// ---- credentials ----
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
Layout.alignment: Qt.AlignTop
|
|
spacing: 6
|
|
|
|
MicroText { text: "OPERATOR" }
|
|
|
|
Field {
|
|
glyph: "@"
|
|
active: userInput.activeFocus
|
|
|
|
TextInput {
|
|
id: userInput
|
|
|
|
anchors.fill: parent
|
|
verticalAlignment: TextInput.AlignVCenter
|
|
enabled: !root.busy && root.inputPrompt === ""
|
|
color: Theme.text
|
|
selectionColor: Theme.accent
|
|
selectedTextColor: Theme.surface
|
|
font.family: Theme.displayFont
|
|
font.pixelSize: 14
|
|
font.letterSpacing: 1
|
|
clip: true
|
|
|
|
onTextEdited: root.userEdited()
|
|
onAccepted: root.focusSecret()
|
|
KeyNavigation.tab: secretInput
|
|
}
|
|
}
|
|
|
|
MicroText {
|
|
Layout.topMargin: 6
|
|
text: root.inputPrompt !== "" ? root.inputPrompt.toUpperCase() : "PASSPHRASE"
|
|
color: root.inputPrompt !== "" ? Theme.accent : Theme.muted
|
|
}
|
|
|
|
Field {
|
|
glyph: ">_"
|
|
active: secretInput.activeFocus
|
|
alert: root.failed
|
|
|
|
TextInput {
|
|
id: secretInput
|
|
|
|
anchors.fill: parent
|
|
verticalAlignment: TextInput.AlignVCenter
|
|
enabled: !root.busy
|
|
color: Theme.text
|
|
selectionColor: Theme.accent
|
|
selectedTextColor: Theme.surface
|
|
font.family: Theme.displayFont
|
|
font.pixelSize: 14
|
|
font.letterSpacing: 1
|
|
clip: true
|
|
|
|
echoMode: root.responseVisible ? TextInput.Normal : TextInput.Password
|
|
passwordCharacter: "▪"
|
|
passwordMaskDelay: 0
|
|
|
|
onAccepted: root.submit()
|
|
KeyNavigation.backtab: userInput
|
|
}
|
|
}
|
|
|
|
// Status line; keeps its height so the panel does not jump.
|
|
MicroText {
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: 4
|
|
Layout.preferredHeight: 12
|
|
text: root.busy && root.message === "" ? "AUTHENTICATING…" : root.message.toUpperCase()
|
|
color: root.messageIsError ? Theme.hot : root.busy ? Theme.accent : Theme.muted
|
|
font.pixelSize: 9
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- footer ----
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
implicitHeight: 1
|
|
color: Theme.textAlpha(0.12)
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 14
|
|
|
|
MicroText { text: "ENTER LOG IN" }
|
|
MicroText { text: "TAB SWITCH FIELD" }
|
|
Item { Layout.fillWidth: true }
|
|
|
|
PowerChip { label: "REBOOT"; action: "reboot" }
|
|
PowerChip { label: "POWER OFF"; action: "poweroff" }
|
|
}
|
|
}
|
|
}
|
|
}
|