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:
@@ -100,6 +100,8 @@ on it, hence the index loops in `HyprChromeShell`.
|
||||
and its panels, with `Bar/Panels/BarPanel.qml` the chamfered chrome they all
|
||||
extend; `Widgets/Polkit/` is the authentication agent and its dialog;
|
||||
`Widgets/Launcher/` is the primary application launcher (`SUPER_L`);
|
||||
`Widgets/Greeter/` is the greetd login screen, run standalone via `greeter.qml`
|
||||
(see `services/desktop/quickshell-greeter.nix`), NOT part of `shell.qml`;
|
||||
`Theme/Theme.qml` is this tree's palette singleton. `DebugWindow.qml` stages a
|
||||
single widget on the secondary monitor for eyeballing it in isolation.
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.HyprChrome.Theme
|
||||
|
||||
// Panel chrome for the login greeter. Same vocabulary as PolkitPanel, mirrored:
|
||||
// cuts on the OTHER diagonal (top-left, bottom-right), registration brackets
|
||||
// instead of detached caps, and a segmented accent spine down the left edge.
|
||||
Item {
|
||||
id: panel
|
||||
|
||||
property string panelId: ""
|
||||
property string title: ""
|
||||
property string meta: ""
|
||||
|
||||
property int chamfer: 20
|
||||
property int padding: 18
|
||||
property int spineWidth: 3
|
||||
property int spineSegments: 9
|
||||
|
||||
// Sweeps the bottom rule while greetd is working.
|
||||
property bool busy: false
|
||||
|
||||
readonly property int headerHeight: 32
|
||||
readonly property int headerPadding: 12
|
||||
|
||||
property real bracketGap: 6
|
||||
property real bracketArm: 14
|
||||
|
||||
readonly property real activeChamfer: Math.max(2, Math.min(panel.chamfer, panel.height / 2 - 1))
|
||||
|
||||
default property alias content: body.data
|
||||
|
||||
implicitHeight: Math.round(body.y + body.height + panel.padding)
|
||||
|
||||
Shape {
|
||||
id: panelShape
|
||||
|
||||
anchors.fill: parent
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: Theme.surface
|
||||
strokeColor: Theme.hair
|
||||
strokeWidth: 1
|
||||
|
||||
startX: panel.activeChamfer; startY: 0
|
||||
PathLine { x: panelShape.width; y: 0 }
|
||||
PathLine { x: panelShape.width; y: panelShape.height - panel.activeChamfer }
|
||||
PathLine { x: panelShape.width - panel.activeChamfer; y: panelShape.height }
|
||||
PathLine { x: 0; y: panelShape.height }
|
||||
PathLine { x: 0; y: panel.activeChamfer }
|
||||
PathLine { x: panel.activeChamfer; y: 0 }
|
||||
}
|
||||
|
||||
// Accent along each cut, inset so it reads as an edge highlight.
|
||||
ShapePath {
|
||||
fillColor: "transparent"
|
||||
strokeColor: Theme.accent
|
||||
strokeWidth: 2
|
||||
capStyle: ShapePath.FlatCap
|
||||
|
||||
startX: 0; startY: panel.activeChamfer
|
||||
PathLine { x: panel.activeChamfer; y: 0 }
|
||||
}
|
||||
|
||||
ShapePath {
|
||||
fillColor: "transparent"
|
||||
strokeColor: Theme.accent
|
||||
strokeWidth: 2
|
||||
capStyle: ShapePath.FlatCap
|
||||
|
||||
startX: panelShape.width; startY: panelShape.height - panel.activeChamfer
|
||||
PathLine { x: panelShape.width - panel.activeChamfer; y: panelShape.height }
|
||||
}
|
||||
|
||||
// Registration brackets on the two square corners, echoing the
|
||||
// backdrop's crosses.
|
||||
ShapePath {
|
||||
fillColor: "transparent"
|
||||
strokeColor: Theme.muted
|
||||
strokeWidth: 1
|
||||
|
||||
startX: panelShape.width + panel.bracketGap - panel.bracketArm
|
||||
startY: -panel.bracketGap
|
||||
PathLine { x: panelShape.width + panel.bracketGap; y: -panel.bracketGap }
|
||||
PathLine { x: panelShape.width + panel.bracketGap; y: -panel.bracketGap + panel.bracketArm }
|
||||
}
|
||||
|
||||
ShapePath {
|
||||
fillColor: "transparent"
|
||||
strokeColor: Theme.muted
|
||||
strokeWidth: 1
|
||||
|
||||
startX: -panel.bracketGap
|
||||
startY: panelShape.height + panel.bracketGap - panel.bracketArm
|
||||
PathLine { x: -panel.bracketGap; y: panelShape.height + panel.bracketGap }
|
||||
PathLine { x: -panel.bracketGap + panel.bracketArm; y: panelShape.height + panel.bracketGap }
|
||||
}
|
||||
}
|
||||
|
||||
// Segmented spine, below the header.
|
||||
Column {
|
||||
x: 0
|
||||
y: panel.headerHeight + 8
|
||||
spacing: 3
|
||||
|
||||
readonly property real segment: (panel.height - panel.headerHeight - 8 - panel.activeChamfer - 8
|
||||
- (panel.spineSegments - 1) * spacing) / panel.spineSegments
|
||||
|
||||
Repeater {
|
||||
model: panel.spineSegments
|
||||
|
||||
Rectangle {
|
||||
required property int index
|
||||
|
||||
width: panel.spineWidth
|
||||
height: Math.max(1, parent.segment)
|
||||
color: Theme.accent
|
||||
// Fades downward.
|
||||
opacity: 1 - index / panel.spineSegments * 0.85
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Header: title, meta, slug chip on the right.
|
||||
Text {
|
||||
x: panel.activeChamfer + panel.headerPadding
|
||||
anchors.verticalCenter: header.verticalCenter
|
||||
text: panel.title
|
||||
color: Theme.text
|
||||
font.family: Theme.displayFont
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
font.letterSpacing: 1.4
|
||||
}
|
||||
|
||||
Row {
|
||||
id: header
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: panel.headerPadding
|
||||
y: Math.round((panel.headerHeight - height) / 2)
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: slugChip.verticalCenter
|
||||
visible: panel.meta.length > 0
|
||||
text: panel.meta
|
||||
color: Theme.muted
|
||||
font.family: Theme.microFont
|
||||
font.pixelSize: 8
|
||||
font.letterSpacing: 0.7
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: slugChip
|
||||
|
||||
width: slugText.implicitWidth + 10
|
||||
height: slugText.implicitHeight + 4
|
||||
color: "transparent"
|
||||
border.width: 1
|
||||
border.color: Theme.accent
|
||||
|
||||
Text {
|
||||
id: slugText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: panel.panelId
|
||||
color: Theme.accent
|
||||
font.family: Theme.microFont
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Header rule: dashed, not solid, to set it apart from the polkit dialog.
|
||||
Row {
|
||||
x: panel.activeChamfer
|
||||
y: panel.headerHeight
|
||||
spacing: 3
|
||||
|
||||
Repeater {
|
||||
model: Math.max(0, Math.floor((panel.width - panel.activeChamfer - 1) / 9))
|
||||
|
||||
Rectangle {
|
||||
width: 6
|
||||
height: 1
|
||||
color: Theme.textAlpha(0.18)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Busy sweep along the bottom edge.
|
||||
Item {
|
||||
x: 0
|
||||
y: panel.height - 2
|
||||
width: panel.width - panel.activeChamfer
|
||||
height: 2
|
||||
clip: true
|
||||
visible: panel.busy
|
||||
|
||||
Rectangle {
|
||||
id: sweep
|
||||
|
||||
width: parent.width / 4
|
||||
height: parent.height
|
||||
color: Theme.accent
|
||||
|
||||
NumberAnimation on x {
|
||||
running: panel.busy
|
||||
loops: Animation.Infinite
|
||||
from: -sweep.width
|
||||
to: panel.width
|
||||
duration: 900
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: body
|
||||
|
||||
x: panel.padding + panel.spineWidth
|
||||
y: panel.headerHeight + panel.padding
|
||||
width: Math.max(0, panel.width - panel.padding * 2 - panel.spineWidth)
|
||||
height: childrenRect.height
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import Quickshell
|
||||
import qs.HyprChrome.Widgets.Greeter
|
||||
|
||||
// greetd greeter entry point: `qs -p greeter.qml`, as the greeter user. See
|
||||
// services/desktop/quickshell-greeter.nix.
|
||||
Scope {
|
||||
Greeter {}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import QtQuick
|
||||
import qs.HyprChrome.Widgets.Greeter
|
||||
|
||||
// Offscreen render of the login panel after a rejected password.
|
||||
//
|
||||
// ./tools/quickshell-preview/render.sh \
|
||||
// tests/LoginPanelHeadless.qml \
|
||||
// .artifacts/quickshell-preview/login-panel.png 700 340
|
||||
LoginContent {
|
||||
width: 640
|
||||
|
||||
hostName: "terra"
|
||||
sessionName: "hyprland"
|
||||
now: new Date(2026, 8, 18, 21, 47, 38)
|
||||
user: "darman"
|
||||
response: "hunter2"
|
||||
|
||||
message: "Authentication failure"
|
||||
messageIsError: true
|
||||
failed: true
|
||||
}
|
||||
Reference in New Issue
Block a user