feat: initial noctalia-greeter release
QuickShell QML login greeter for greetd, styled after noctalia-shell lockscreen. Runs under niri compositor as _greeter user. Theme is live-synced from noctalia config via runit service. - shell.qml: QuickShell greeter UI + greetd auth flow - sync/: inotifywait theme sync daemon + runit service - greetd/: niri compositor config + wrapper scripts - install.sh: deployment helper - test/check-syntax.sh: 19 syntax/structural checks (0 failures) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
65
README.md
Normal file
65
README.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# noctalia-greeter
|
||||||
|
|
||||||
|
A QuickShell QML login greeter for [greetd](https://git.sr.ht/~kennylevinsen/greetd), styled to match the [noctalia-shell](https://github.com/nocturnalastro/noctalia-shell) lockscreen.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
greetd (VT7, _greeter user)
|
||||||
|
└── dbus-run-session niri --config /etc/greetd/niri-greeter.kdl
|
||||||
|
└── /etc/greetd/start-greeter.sh
|
||||||
|
└── qs -c ~/.config/noctalia-greeter/ (shell.qml)
|
||||||
|
reads /var/lib/noctalia-greeter-theme/{colors.json,wallpaper.jpg}
|
||||||
|
|
||||||
|
noctalia-greeter-sync (runit, moze user)
|
||||||
|
inotifywait ~/.config/noctalia/ + ~/.cache/noctalia/
|
||||||
|
→ copies colors.json + wallpaper on any noctalia style change
|
||||||
|
→ writes to /var/lib/noctalia-greeter-theme/ (world-readable)
|
||||||
|
```
|
||||||
|
|
||||||
|
The `_greeter` user never touches `/home/moze`. Theme data is staged to a world-readable directory by the sync service running as moze.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| Source | Deployed to |
|
||||||
|
|--------|-------------|
|
||||||
|
| `shell.qml` | `~/.config/noctalia-greeter/shell.qml` |
|
||||||
|
| `sync/noctalia-greeter-sync` | `/usr/local/bin/noctalia-greeter-sync` |
|
||||||
|
| `sync/run` | `/etc/sv/noctalia-greeter-sync/run` |
|
||||||
|
| `greetd/niri-greeter.kdl` | `/etc/greetd/niri-greeter.kdl` |
|
||||||
|
| `greetd/start-greeter.sh` | `/etc/greetd/start-greeter.sh` |
|
||||||
|
| `greetd/config.toml.example` | `/etc/greetd/config.toml` (edit before deploying) |
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo ./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires: `greetd`, `niri`, `quickshell` (`qs`), `dbus`, `inotify-tools`.
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
Edit `/etc/greetd/config.toml`:
|
||||||
|
```toml
|
||||||
|
[terminal]
|
||||||
|
vt = 7
|
||||||
|
|
||||||
|
[default_session]
|
||||||
|
command = "sway --config /etc/greetd/sway-greeter.conf"
|
||||||
|
user = "_greeter"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Theme sync
|
||||||
|
|
||||||
|
Change colors/wallpaper in noctalia shell → greeter auto-updates next login (no restart needed).
|
||||||
|
|
||||||
|
The sync service watches:
|
||||||
|
- `~/.config/noctalia/colors.json`
|
||||||
|
- `~/.cache/noctalia/wallpapers.json`
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./test/check-syntax.sh
|
||||||
|
```
|
||||||
6
greetd/config.toml.example
Normal file
6
greetd/config.toml.example
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[terminal]
|
||||||
|
vt = 7
|
||||||
|
|
||||||
|
[default_session]
|
||||||
|
command = "/usr/sbin/dbus-run-session /usr/sbin/niri --config /etc/greetd/niri-greeter.kdl"
|
||||||
|
user = "_greeter"
|
||||||
3
greetd/niri-greeter.kdl
Normal file
3
greetd/niri-greeter.kdl
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Minimal niri config for the greetd greeter session.
|
||||||
|
// Spawns the QuickShell greeter; quits niri when qs exits.
|
||||||
|
spawn-at-startup "sh" "-c" "/etc/greetd/start-greeter.sh"
|
||||||
4
greetd/start-greeter.sh
Executable file
4
greetd/start-greeter.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Launch QuickShell greeter; quit niri when it exits (NIRI_SOCKET inherited from niri env).
|
||||||
|
/usr/sbin/qs -c /home/moze/.config/noctalia-greeter
|
||||||
|
/usr/sbin/niri msg action quit --skip-confirmation 2>/dev/null || true
|
||||||
39
install.sh
Executable file
39
install.sh
Executable file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# install.sh — deploy noctalia-greeter to system paths
|
||||||
|
set -e
|
||||||
|
|
||||||
|
REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
echo "Run as root: sudo ./install.sh" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Installing noctalia-greeter..."
|
||||||
|
|
||||||
|
# QuickShell config (read by qs as _greeter)
|
||||||
|
install -Dm644 "$REPO_DIR/shell.qml" /home/moze/.config/noctalia-greeter/shell.qml
|
||||||
|
chown moze:moze /home/moze/.config/noctalia-greeter/shell.qml
|
||||||
|
|
||||||
|
# Sync daemon
|
||||||
|
install -Dm755 "$REPO_DIR/sync/noctalia-greeter-sync" /usr/local/bin/noctalia-greeter-sync
|
||||||
|
|
||||||
|
# Runit service
|
||||||
|
install -Dm755 "$REPO_DIR/sync/run" /etc/sv/noctalia-greeter-sync/run
|
||||||
|
|
||||||
|
# Greeter compositor config
|
||||||
|
install -Dm644 "$REPO_DIR/greetd/niri-greeter.kdl" /etc/greetd/niri-greeter.kdl
|
||||||
|
install -Dm755 "$REPO_DIR/greetd/start-greeter.sh" /etc/greetd/start-greeter.sh
|
||||||
|
|
||||||
|
# Theme staging dir
|
||||||
|
install -d -m755 /var/lib/noctalia-greeter-theme
|
||||||
|
chown moze:moze /var/lib/noctalia-greeter-theme
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "==> Next steps:"
|
||||||
|
echo " 1. Edit /etc/greetd/config.toml (see greetd/config.toml.example)"
|
||||||
|
echo " 2. Enable sync service: ln -s /etc/sv/noctalia-greeter-sync /var/service/"
|
||||||
|
echo " 3. Run initial sync: su -s /bin/sh moze -c /usr/local/bin/noctalia-greeter-sync &"
|
||||||
|
echo " 4. Restart greetd: sv restart greetd"
|
||||||
|
echo ""
|
||||||
|
echo "Done."
|
||||||
383
shell.qml
Normal file
383
shell.qml
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
pragma Singleton
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Greetd
|
||||||
|
import Quickshell.Io
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Effects
|
||||||
|
|
||||||
|
ShellRoot {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// ── Theme (synced by noctalia-greeter-sync service) ──────────────────────
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: colorsFile
|
||||||
|
path: "/var/lib/noctalia-greeter-theme/colors.json"
|
||||||
|
blockLoading: true
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var nc: {
|
||||||
|
try {
|
||||||
|
return JSON.parse(colorsFile.text())
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
mPrimary: "#76daa2", mOnPrimary: "#003920",
|
||||||
|
mSurface: "#111412", mOnSurface: "#e1e3de",
|
||||||
|
mSurfaceVariant: "#1d201e", mOnSurfaceVariant: "#c0c9c0",
|
||||||
|
mOutline: "#404942", mShadow: "#000000",
|
||||||
|
mError: "#ffb4ab", mOnError: "#690005",
|
||||||
|
mTertiary: "#a3cddc", mOnTertiary: "#043541"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property string wallpaperPath: "/var/lib/noctalia-greeter-theme/wallpaper.jpg"
|
||||||
|
|
||||||
|
// ── Auth state ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
property string password: ""
|
||||||
|
property string statusMsg: ""
|
||||||
|
property bool statusErr: false
|
||||||
|
property bool waitingForPw: false
|
||||||
|
property bool launching: false
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
Greetd.createSession("moze")
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: Greetd
|
||||||
|
|
||||||
|
function onAuthMessage(message, isError, responseRequired, echoResponse) {
|
||||||
|
if (responseRequired) {
|
||||||
|
root.waitingForPw = true
|
||||||
|
if (!root.statusErr)
|
||||||
|
root.statusMsg = ""
|
||||||
|
} else {
|
||||||
|
root.statusMsg = message || ""
|
||||||
|
root.statusErr = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onReadyToLaunch() {
|
||||||
|
root.launching = true
|
||||||
|
root.statusMsg = "Welcome back!"
|
||||||
|
root.statusErr = false
|
||||||
|
Greetd.launch(
|
||||||
|
["/usr/sbin/dbus-run-session", "/usr/sbin/niri", "--session"],
|
||||||
|
[],
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAuthFailure(message) {
|
||||||
|
root.statusMsg = message || "Authentication failed"
|
||||||
|
root.statusErr = true
|
||||||
|
root.password = ""
|
||||||
|
root.waitingForPw = false
|
||||||
|
restartTimer.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onError(error) {
|
||||||
|
root.statusMsg = "Session error: " + error
|
||||||
|
root.statusErr = true
|
||||||
|
restartTimer.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: restartTimer
|
||||||
|
interval: 2000
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
root.statusMsg = ""
|
||||||
|
root.statusErr = false
|
||||||
|
Greetd.createSession("moze")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitPassword() {
|
||||||
|
if (!root.waitingForPw || root.password.length === 0) return
|
||||||
|
Greetd.respond(root.password)
|
||||||
|
root.password = ""
|
||||||
|
root.waitingForPw = false
|
||||||
|
root.statusMsg = "Verifying…"
|
||||||
|
root.statusErr = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Clocks ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
property date clockNow: new Date()
|
||||||
|
property date clockDate: new Date()
|
||||||
|
|
||||||
|
Timer { interval: 1000; running: true; repeat: true; onTriggered: root.clockNow = new Date() }
|
||||||
|
Timer { interval: 60000; running: true; repeat: true; onTriggered: root.clockDate = new Date() }
|
||||||
|
|
||||||
|
// ── Greeter window ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Variants {
|
||||||
|
model: Quickshell.screens
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
required property var modelData
|
||||||
|
screen: modelData
|
||||||
|
exclusiveZone: -1
|
||||||
|
anchors { top: true; bottom: true; left: true; right: true }
|
||||||
|
color: "transparent"
|
||||||
|
keyboardFocus: KeyboardFocus.Exclusive
|
||||||
|
|
||||||
|
// ── Background ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "#000000"
|
||||||
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.fill: parent
|
||||||
|
source: root.wallpaperPath
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
smooth: true
|
||||||
|
mipmap: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gradient overlay (matches noctalia LockScreenBackground)
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
gradient: Gradient {
|
||||||
|
GradientStop { position: 0.0; color: Qt.alpha(root.nc.mShadow, 0.45) }
|
||||||
|
GradientStop { position: 0.3; color: Qt.alpha(root.nc.mShadow, 0.22) }
|
||||||
|
GradientStop { position: 0.7; color: Qt.alpha(root.nc.mShadow, 0.27) }
|
||||||
|
GradientStop { position: 1.0; color: Qt.alpha(root.nc.mShadow, 0.55) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Header (mirrors LockScreenHeader) ────────────────────────────
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: header
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: 100
|
||||||
|
width: Math.max(520, headerRow.implicitWidth + 40)
|
||||||
|
height: headerRow.implicitHeight + 32
|
||||||
|
radius: 14
|
||||||
|
color: Qt.alpha(root.nc.mSurface, 0.88)
|
||||||
|
border.color: Qt.alpha(root.nc.mOutline, 0.22)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: headerRow
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 18
|
||||||
|
spacing: 22
|
||||||
|
|
||||||
|
// Avatar
|
||||||
|
Rectangle {
|
||||||
|
Layout.preferredWidth: 70
|
||||||
|
Layout.preferredHeight: 70
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
radius: width / 2
|
||||||
|
color: "transparent"
|
||||||
|
border.color: Qt.alpha(root.nc.mPrimary, 0.85)
|
||||||
|
border.width: 2
|
||||||
|
|
||||||
|
// Fallback person icon
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: ""
|
||||||
|
font.pixelSize: 32
|
||||||
|
font.family: "Symbols Nerd Font"
|
||||||
|
color: root.nc.mOnSurfaceVariant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Welcome + date
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Welcome back, Moze!"
|
||||||
|
font.pixelSize: 20
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: root.nc.mOnSurface
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: {
|
||||||
|
const s = Qt.locale().toString(root.clockDate, "dddd, MMMM d")
|
||||||
|
return s.charAt(0).toUpperCase() + s.slice(1)
|
||||||
|
}
|
||||||
|
font.pixelSize: 15
|
||||||
|
color: root.nc.mOnSurfaceVariant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
|
||||||
|
// Stacked clock (hh / mm, matches clockStyle: "custom")
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
spacing: -6
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Qt.locale().toString(root.clockNow, "hh\nmm").split("\n")
|
||||||
|
Text {
|
||||||
|
required property string modelData
|
||||||
|
text: modelData
|
||||||
|
font.pixelSize: 32
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: root.nc.mOnSurface
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Password panel ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.top: header.bottom
|
||||||
|
anchors.topMargin: parent.height * 0.10
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 380
|
||||||
|
height: 64
|
||||||
|
radius: 14
|
||||||
|
color: Qt.alpha(root.nc.mSurfaceVariant, 0.92)
|
||||||
|
border.color: {
|
||||||
|
if (root.statusErr) return Qt.alpha(root.nc.mError, 0.75)
|
||||||
|
if (root.waitingForPw) return Qt.alpha(root.nc.mPrimary, 0.85)
|
||||||
|
return Qt.alpha(root.nc.mOutline, 0.35)
|
||||||
|
}
|
||||||
|
border.width: 1.5
|
||||||
|
Behavior on border.color { ColorAnimation { duration: 180 } }
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 18
|
||||||
|
anchors.rightMargin: 18
|
||||||
|
spacing: 12
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.statusErr ? "" : (root.waitingForPw ? "" : "")
|
||||||
|
font.pixelSize: 18
|
||||||
|
font.family: "Symbols Nerd Font"
|
||||||
|
color: root.statusErr
|
||||||
|
? root.nc.mError
|
||||||
|
: root.nc.mOnSurfaceVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: {
|
||||||
|
if (root.launching) return "Logging in…"
|
||||||
|
if (root.password.length) return "●".repeat(root.password.length)
|
||||||
|
if (root.waitingForPw) return "Enter password…"
|
||||||
|
return "Please wait…"
|
||||||
|
}
|
||||||
|
font.pixelSize: root.password.length ? 22 : 15
|
||||||
|
letterSpacing: root.password.length ? 6 : 0
|
||||||
|
color: root.password.length
|
||||||
|
? root.nc.mOnSurface
|
||||||
|
: root.nc.mOnSurfaceVariant
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Behavior on font.pixelSize { NumberAnimation { duration: 80 } }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blinking cursor when idle waiting for input
|
||||||
|
Rectangle {
|
||||||
|
width: 2; height: 28
|
||||||
|
color: root.nc.mPrimary
|
||||||
|
visible: root.waitingForPw && root.password.length === 0
|
||||||
|
property real op: 1.0
|
||||||
|
opacity: op
|
||||||
|
SequentialAnimation on op {
|
||||||
|
running: root.waitingForPw && root.password.length === 0
|
||||||
|
loops: Animation.Infinite
|
||||||
|
NumberAnimation { to: 0; duration: 500; easing.type: Easing.InOutQuad }
|
||||||
|
NumberAnimation { to: 1; duration: 500; easing.type: Easing.InOutQuad }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status / error pill
|
||||||
|
Rectangle {
|
||||||
|
width: 380
|
||||||
|
height: 38
|
||||||
|
radius: 10
|
||||||
|
visible: root.statusMsg !== ""
|
||||||
|
opacity: visible ? 1.0 : 0.0
|
||||||
|
Behavior on opacity { NumberAnimation { duration: 200 } }
|
||||||
|
color: root.statusErr
|
||||||
|
? Qt.alpha(root.nc.mError, 0.13)
|
||||||
|
: Qt.alpha(root.nc.mTertiary, 0.13)
|
||||||
|
border.color: root.statusErr
|
||||||
|
? Qt.alpha(root.nc.mError, 0.4)
|
||||||
|
: Qt.alpha(root.nc.mTertiary, 0.35)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - 24
|
||||||
|
text: root.statusMsg
|
||||||
|
font.pixelSize: 13
|
||||||
|
color: root.statusErr ? root.nc.mError : root.nc.mOnSurface
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Keyboard capture (invisible, full-screen) ─────────────────────
|
||||||
|
|
||||||
|
TextInput {
|
||||||
|
id: pwInput
|
||||||
|
anchors.fill: parent
|
||||||
|
opacity: 0
|
||||||
|
echoMode: TextInput.Password
|
||||||
|
passwordMaskDelay: 0
|
||||||
|
enabled: !root.launching
|
||||||
|
|
||||||
|
onTextChanged: {
|
||||||
|
if (root.password !== text)
|
||||||
|
root.password = text
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root
|
||||||
|
function onPasswordChanged() {
|
||||||
|
if (pwInput.text !== root.password)
|
||||||
|
pwInput.text = root.password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: function(event) {
|
||||||
|
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||||
|
root.submitPassword()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Escape) {
|
||||||
|
root.password = ""
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: forceActiveFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.AllButtons
|
||||||
|
onClicked: pwInput.forceActiveFocus()
|
||||||
|
z: -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
sync/noctalia-greeter-sync
Executable file
49
sync/noctalia-greeter-sync
Executable file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Syncs noctalia theme to /var/lib/noctalia-greeter-theme/ for the _greeter user.
|
||||||
|
export HOME=/home/moze
|
||||||
|
export USER=moze
|
||||||
|
|
||||||
|
THEME_DIR="/var/lib/noctalia-greeter-theme"
|
||||||
|
COLORS="$HOME/.config/noctalia/colors.json"
|
||||||
|
WALLPAPERS_JSON="$HOME/.cache/noctalia/wallpapers.json"
|
||||||
|
|
||||||
|
do_sync() {
|
||||||
|
if [ -f "$COLORS" ]; then
|
||||||
|
cp -f "$COLORS" "$THEME_DIR/colors.json" 2>/dev/null
|
||||||
|
chmod 644 "$THEME_DIR/colors.json" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$WALLPAPERS_JSON" ]; then
|
||||||
|
WP=$(python3 -c "
|
||||||
|
import json, sys
|
||||||
|
try:
|
||||||
|
d = json.load(open('$WALLPAPERS_JSON'))
|
||||||
|
m = d.get('wallpapers', {})
|
||||||
|
for s in ['eDP-1', 'DP-1'] + list(m.keys()):
|
||||||
|
if s in m:
|
||||||
|
p = m[s].get('dark') or m[s].get('light') or ''
|
||||||
|
if p:
|
||||||
|
print(p)
|
||||||
|
sys.exit(0)
|
||||||
|
fb = d.get('defaultWallpaper', '')
|
||||||
|
if fb: print(fb)
|
||||||
|
except Exception as e:
|
||||||
|
sys.stderr.write(str(e) + chr(10))
|
||||||
|
" 2>/dev/null)
|
||||||
|
if [ -n "$WP" ] && [ -f "$WP" ]; then
|
||||||
|
cp -f "$WP" "$THEME_DIR/wallpaper.jpg" 2>/dev/null
|
||||||
|
chmod 644 "$THEME_DIR/wallpaper.jpg" 2>/dev/null
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
do_sync
|
||||||
|
|
||||||
|
exec inotifywait -m -q \
|
||||||
|
-e close_write,moved_to,create \
|
||||||
|
"$HOME/.config/noctalia/" \
|
||||||
|
"$HOME/.cache/noctalia/" 2>/dev/null |
|
||||||
|
while IFS= read -r _line; do
|
||||||
|
sleep 0.3
|
||||||
|
do_sync
|
||||||
|
done
|
||||||
3
sync/run
Executable file
3
sync/run
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
exec 2>&1
|
||||||
|
exec chpst -u moze env HOME=/home/moze USER=moze /usr/local/bin/noctalia-greeter-sync
|
||||||
197
test/check-syntax.sh
Executable file
197
test/check-syntax.sh
Executable file
@@ -0,0 +1,197 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# test/check-syntax.sh — static syntax checks for noctalia-greeter
|
||||||
|
# No runtime required. Run from repo root or any location.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
|
||||||
|
ok() { echo " [PASS] $1"; PASS=$((PASS+1)); }
|
||||||
|
fail() { echo " [FAIL] $1"; echo " $2"; FAIL=$((FAIL+1)); }
|
||||||
|
|
||||||
|
echo "=== noctalia-greeter syntax checks ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── Shell scripts ──────────────────────────────────────────────────────────────
|
||||||
|
echo "-- Shell syntax (bash -n) --"
|
||||||
|
|
||||||
|
for f in \
|
||||||
|
"sync/noctalia-greeter-sync" \
|
||||||
|
"sync/run" \
|
||||||
|
"greetd/start-greeter.sh" \
|
||||||
|
"install.sh"
|
||||||
|
do
|
||||||
|
path="$REPO_DIR/$f"
|
||||||
|
if [ ! -f "$path" ]; then
|
||||||
|
fail "$f" "file not found: $path"
|
||||||
|
elif out=$(bash -n "$path" 2>&1); then
|
||||||
|
ok "$f"
|
||||||
|
else
|
||||||
|
fail "$f" "$out"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Embedded Python snippet ────────────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "-- Python snippet in noctalia-greeter-sync --"
|
||||||
|
|
||||||
|
SYNC="$REPO_DIR/sync/noctalia-greeter-sync"
|
||||||
|
if [ -f "$SYNC" ]; then
|
||||||
|
# Extract python3 heredoc: lines between 'python3 -c "' and closing '"'
|
||||||
|
PY_SNIPPET=$(sed -n '/python3 -c "/,/^" /{ /python3 -c "/d; /^" /d; p }' "$SYNC")
|
||||||
|
if [ -n "$PY_SNIPPET" ]; then
|
||||||
|
if out=$(echo "$PY_SNIPPET" | python3 -c "
|
||||||
|
import ast, sys
|
||||||
|
src = sys.stdin.read()
|
||||||
|
try:
|
||||||
|
ast.parse(src)
|
||||||
|
print('ok')
|
||||||
|
except SyntaxError as e:
|
||||||
|
print('SyntaxError: ' + str(e))
|
||||||
|
sys.exit(1)
|
||||||
|
" 2>&1); then
|
||||||
|
ok "embedded python snippet (ast.parse)"
|
||||||
|
else
|
||||||
|
fail "embedded python snippet" "$out"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "embedded python snippet" "could not extract snippet from $SYNC"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "embedded python snippet" "sync script not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Niri config ────────────────────────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "-- Niri config (niri validate) --"
|
||||||
|
|
||||||
|
KDL="$REPO_DIR/greetd/niri-greeter.kdl"
|
||||||
|
if [ ! -f "$KDL" ]; then
|
||||||
|
fail "niri-greeter.kdl" "file not found"
|
||||||
|
elif ! command -v niri >/dev/null 2>&1; then
|
||||||
|
echo " [SKIP] niri-greeter.kdl — niri not in PATH"
|
||||||
|
elif out=$(niri validate -c "$KDL" 2>&1); then
|
||||||
|
ok "niri-greeter.kdl"
|
||||||
|
else
|
||||||
|
fail "niri-greeter.kdl" "$out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── QML structural check ───────────────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "-- QML structural check --"
|
||||||
|
|
||||||
|
QML="$REPO_DIR/shell.qml"
|
||||||
|
if [ ! -f "$QML" ]; then
|
||||||
|
fail "shell.qml" "file not found"
|
||||||
|
elif command -v qmllint >/dev/null 2>&1; then
|
||||||
|
if out=$(qmllint "$QML" 2>&1); then
|
||||||
|
ok "shell.qml (qmllint)"
|
||||||
|
else
|
||||||
|
fail "shell.qml (qmllint)" "$out"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Fallback: brace/bracket balance via Python
|
||||||
|
if out=$(python3 - "$QML" << 'EOF'
|
||||||
|
import sys
|
||||||
|
|
||||||
|
path = sys.argv[1]
|
||||||
|
src = open(path).read()
|
||||||
|
depth = 0
|
||||||
|
in_str = False
|
||||||
|
str_char = None
|
||||||
|
i = 0
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
while i < len(src):
|
||||||
|
c = src[i]
|
||||||
|
if in_str:
|
||||||
|
if c == '\\':
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
if c == str_char:
|
||||||
|
in_str = False
|
||||||
|
else:
|
||||||
|
if c in ('"', "'"):
|
||||||
|
in_str = True
|
||||||
|
str_char = c
|
||||||
|
elif c == '{':
|
||||||
|
depth += 1
|
||||||
|
elif c == '}':
|
||||||
|
depth -= 1
|
||||||
|
if depth < 0:
|
||||||
|
line = src[:i].count('\n') + 1
|
||||||
|
errors.append(f"line {line}: unexpected '}}' (depth went negative)")
|
||||||
|
depth = 0
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if depth != 0:
|
||||||
|
errors.append(f"unbalanced braces: depth={depth} at EOF")
|
||||||
|
|
||||||
|
# Check imports present
|
||||||
|
required = ["import Quickshell", "Quickshell.Services.Greetd", "ShellRoot"]
|
||||||
|
for r in required:
|
||||||
|
if r not in src:
|
||||||
|
errors.append(f"missing expected token: '{r}'")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
for e in errors:
|
||||||
|
print(e)
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print("ok")
|
||||||
|
EOF
|
||||||
|
2>&1); then
|
||||||
|
ok "shell.qml (brace balance + required imports)"
|
||||||
|
else
|
||||||
|
fail "shell.qml" "$out"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Required files present ─────────────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "-- Required files present --"
|
||||||
|
|
||||||
|
for f in \
|
||||||
|
"shell.qml" \
|
||||||
|
"sync/noctalia-greeter-sync" \
|
||||||
|
"sync/run" \
|
||||||
|
"greetd/niri-greeter.kdl" \
|
||||||
|
"greetd/start-greeter.sh" \
|
||||||
|
"greetd/config.toml.example" \
|
||||||
|
"install.sh" \
|
||||||
|
"README.md"
|
||||||
|
do
|
||||||
|
path="$REPO_DIR/$f"
|
||||||
|
if [ -f "$path" ]; then
|
||||||
|
ok "$f exists"
|
||||||
|
else
|
||||||
|
fail "$f" "missing: $path"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Permissions check ──────────────────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "-- Executable bits --"
|
||||||
|
|
||||||
|
for f in \
|
||||||
|
"sync/noctalia-greeter-sync" \
|
||||||
|
"sync/run" \
|
||||||
|
"greetd/start-greeter.sh" \
|
||||||
|
"install.sh"
|
||||||
|
do
|
||||||
|
path="$REPO_DIR/$f"
|
||||||
|
if [ ! -f "$path" ]; then
|
||||||
|
fail "$f" "not found"
|
||||||
|
elif [ -x "$path" ]; then
|
||||||
|
ok "$f is executable"
|
||||||
|
else
|
||||||
|
fail "$f" "not executable (chmod +x $path)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Summary ────────────────────────────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "=== Results: $PASS passed, $FAIL failed ==="
|
||||||
|
|
||||||
|
[ "$FAIL" -eq 0 ]
|
||||||
Reference in New Issue
Block a user