NoSignal-OS/os updates/sddm-theme/caelestia/Main.qml
28allday 19fd794b6b NoSignal — fully-offline Arch → Hyprland desktop installer
Single-script builder (nosignal.sh) that turns a stock Arch Linux ISO into a
fully-offline installer for a themed Hyprland + caelestia (Quickshell) desktop:
matching SDDM greeter, Btrfs/Limine bootable snapshots, chwd-style GPU
detection, and a curated "os updates" layer (keybind cheatsheet, settings
panels, system polish, on-box management skill). See README.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 11:03:10 +01:00

307 lines
10 KiB
QML
Executable file

import QtQuick
// Caelestia-style SDDM greeter: Material palette + wallpaper + font all come
// from theme.conf, which sddm-theme-sync regenerates from the desktop's live
// caelestia scheme (~/.local/state/caelestia/scheme.json). Pure QtQuick — no
// QtQuick.Controls styles, no SddmComponents — so there is nothing to break.
Item {
id: root
property int sessionIndex: sessionModel.lastIndex
property var sessionNames: ({})
property string sessionName: ""
// Gaming Mode is entered from the DESKTOP only (Super+Shift+S → DeckShift
// switch); logging into the gamescope session from the greeter would
// bypass that flow entirely. Hide gamescope sessions from the cycler, and
// never preselect one (SDDM remembers the last session, which is a
// gamescope session after a reboot from Gaming Mode).
// Hidden: all gamescope sessions ("Gaming Mode (ChimeraOS)", "Steam Big
// Picture" x2) — Gaming Mode is desktop-only (Super+Shift+S) — and the
// plain "Hyprland" session, which loses the uwsm-managed environment the
// caelestia shell needs (a shell-less desktop is all you'd get).
function isAllowed(i) {
var n = sessionNames[i]
return n !== undefined && n !== "Hyprland" && !/gam(ing|escope)|steam|big picture/i.test(n)
}
function allowedCount() {
var total = 0
for (var i in sessionNames)
if (isAllowed(parseInt(i)))
total++
return total
}
function nextAllowed(from) {
var count = sessionModel.rowCount()
for (var step = 1; step <= count; step++) {
var i = (from + step) % count
if (isAllowed(i))
return i
}
return from
}
function refreshSessionName() {
if (sessionNames[sessionIndex] !== undefined && !isAllowed(sessionIndex))
sessionIndex = nextAllowed(sessionIndex)
sessionName = sessionNames[sessionIndex] !== undefined ? sessionNames[sessionIndex] : "Default"
}
function tryLogin() {
errorText.text = ""
sddm.login(userInput.text, passInput.text, sessionIndex)
}
// The exact font the desktop shell uses (Google Sans Flex, bundled inside
// the caelestia shell tree); falls back to the configured family (Rubik).
FontLoader {
id: uiFont
source: config.fontFile ? "file://" + config.fontFile : ""
}
property string fontFamily: uiFont.status === FontLoader.Ready ? uiFont.name : config.fontFallback
// Invisible probe to collect session display names from sessionModel.
Repeater {
model: sessionModel
delegate: Item {
visible: false
Component.onCompleted: {
root.sessionNames[index] = name
root.refreshSessionName()
}
}
}
Connections {
target: sddm
function onLoginFailed() {
errorText.text = "Incorrect password — try again"
passInput.text = ""
passInput.forceActiveFocus()
}
}
Image {
anchors.fill: parent
source: config.background
fillMode: Image.PreserveAspectCrop
Rectangle {
anchors.fill: parent
color: "#000000"
opacity: 0.32
}
}
// Clock
Column {
anchors.horizontalCenter: parent.horizontalCenter
y: parent.height * 0.14
spacing: 4
Text {
id: clock
anchors.horizontalCenter: parent.horizontalCenter
font.family: root.fontFamily
font.pixelSize: 96
font.weight: Font.Light
color: config.text
text: Qt.formatTime(new Date(), "HH:mm")
}
Text {
id: dateText
anchors.horizontalCenter: parent.horizontalCenter
font.family: root.fontFamily
font.pixelSize: 20
color: config.subtext
text: Qt.formatDate(new Date(), "dddd, d MMMM")
}
}
Timer {
interval: 5000; running: true; repeat: true
onTriggered: {
clock.text = Qt.formatTime(new Date(), "HH:mm")
dateText.text = Qt.formatDate(new Date(), "dddd, d MMMM")
}
}
// Login card
Rectangle {
id: card
anchors.horizontalCenter: parent.horizontalCenter
y: parent.height * 0.42
width: 380
height: cardColumn.height + 48
radius: 24
color: config.surfaceContainer
opacity: 0.96
Column {
id: cardColumn
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 48
spacing: 14
// Username
Rectangle {
width: parent.width
height: 48
radius: 16
color: config.surfaceContainerHigh
border.color: userInput.activeFocus ? config.primary : config.outline
border.width: 1
TextInput {
id: userInput
anchors.fill: parent
anchors.leftMargin: 18
anchors.rightMargin: 18
verticalAlignment: TextInput.AlignVCenter
font.family: root.fontFamily
font.pixelSize: 16
color: config.text
selectionColor: config.primary
selectedTextColor: config.onPrimary
text: userModel.lastUser
clip: true
KeyNavigation.tab: passInput
}
}
// Password
Rectangle {
width: parent.width
height: 48
radius: 16
color: config.surfaceContainerHigh
border.color: passInput.activeFocus ? config.primary : config.outline
border.width: 1
TextInput {
id: passInput
anchors.fill: parent
anchors.leftMargin: 18
anchors.rightMargin: 18
verticalAlignment: TextInput.AlignVCenter
font.family: root.fontFamily
font.pixelSize: 16
echoMode: TextInput.Password
passwordCharacter: "•"
color: config.text
selectionColor: config.primary
selectedTextColor: config.onPrimary
clip: true
focus: true
KeyNavigation.tab: userInput
onAccepted: root.tryLogin()
}
Text {
anchors.fill: parent
anchors.leftMargin: 18
verticalAlignment: Text.AlignVCenter
visible: passInput.text.length === 0 && !passInput.activeFocus
font.family: root.fontFamily
font.pixelSize: 16
color: config.subtext
text: "Password"
}
}
Text {
id: errorText
width: parent.width
horizontalAlignment: Text.AlignHCenter
visible: text.length > 0
font.family: root.fontFamily
font.pixelSize: 14
color: config.error
text: ""
wrapMode: Text.Wrap
}
// Login button
Rectangle {
width: parent.width
height: 48
radius: 16
color: loginArea.pressed ? Qt.darker(config.primary, 1.15) : config.primary
Text {
anchors.centerIn: parent
font.family: root.fontFamily
font.pixelSize: 16
font.weight: Font.Medium
color: config.onPrimary
text: "Log in"
}
MouseArea {
id: loginArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.tryLogin()
}
}
// Session cycler (hidden when only one session is selectable)
Text {
width: parent.width
horizontalAlignment: Text.AlignHCenter
// referencing sessionName makes the binding re-evaluate as
// the session list is populated
visible: root.sessionName !== "" && root.allowedCount() > 1
font.family: root.fontFamily
font.pixelSize: 13
color: sessionArea.containsMouse ? config.text : config.subtext
text: "Session: " + root.sessionName + " ⟳"
MouseArea {
id: sessionArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.sessionIndex = root.nextAllowed(root.sessionIndex)
root.refreshSessionName()
}
}
}
}
}
// Power controls
Row {
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 28
spacing: 24
Repeater {
model: [
{ label: "Sleep", enabled: sddm.canSuspend, act: function() { sddm.suspend() } },
{ label: "Restart", enabled: sddm.canReboot, act: function() { sddm.reboot() } },
{ label: "Shut down", enabled: sddm.canPowerOff, act: function() { sddm.powerOff() } }
]
delegate: Text {
visible: modelData.enabled
font.family: root.fontFamily
font.pixelSize: 14
color: powerArea.containsMouse ? config.text : config.subtext
text: modelData.label
MouseArea {
id: powerArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: modelData.act()
}
}
}
}
Component.onCompleted: passInput.forceActiveFocus()
}