We now own the pinned shell, so the runtime patches that the pacman-hook + patch-script machinery re-applied after every caelestia-shell upgrade are baked directly into source. Folds in all five caelestia-shell-targeting patches from the builder's `os updates/`: - Lock PAM (F1, blocker): ship faillock-free assets/pam.d/caelestia and point modules/lock/Pam.qml's passwd PamContext at it (config "passwd" -> "caelestia"). A desktop screen-lock must never lock the user out of their own session. - Updates page: add modules/nexus/pages/UpdatesPage.qml + register it in the first System slot of PageCompRegistry.qml. - Additions page: add modules/nexus/pages/AdditionsPage.qml + register it in the Plugins slot; relabel Plugins -> Additions in PageRegistry.qml. - Sudo toggle: add modules/nexus/common/SudoToggleRow.qml + insert it into ServicesPage.qml after the Smart colour scheme toggle. - Wi-Fi wrong-password recovery: NetworkConnection.qml saved-profile branch now passes a real callback that forgets the bad profile and reopens the dialog. The builder will drop the corresponding patch-*.sh calls + pacman hooks and bump NOSIGNAL_SHELL_COMMIT to this commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
198 lines
5.1 KiB
QML
198 lines
5.1 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.Wayland
|
|
import Quickshell.Services.Pam
|
|
import Caelestia.Config
|
|
|
|
Scope {
|
|
id: root
|
|
|
|
required property WlSessionLock lock
|
|
|
|
readonly property alias passwd: passwd
|
|
readonly property alias fprint: fprint
|
|
property string lockMessage
|
|
property string state
|
|
property string fprintState
|
|
property string buffer
|
|
|
|
signal flashMsg
|
|
|
|
function handleKey(event: KeyEvent): void {
|
|
if (passwd.active || state === "max")
|
|
return;
|
|
|
|
if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {
|
|
passwd.start();
|
|
} else if (event.key === Qt.Key_Backspace) {
|
|
if (event.modifiers & Qt.ControlModifier) {
|
|
buffer = "";
|
|
} else {
|
|
buffer = buffer.slice(0, -1);
|
|
}
|
|
} else if (/^[^\x00-\x1F\x7F-\x9F]+$/.test(event.text)) {
|
|
// Allow anything except control characters
|
|
buffer += event.text;
|
|
}
|
|
}
|
|
|
|
PamContext {
|
|
id: passwd
|
|
|
|
// NoSignal: faillock-free lock auth (finding F1). The stock "passwd"
|
|
// service routes through pam_faillock, which can lock the user out of
|
|
// their OWN session after a few failed unlocks — refusing even the
|
|
// correct password until /run/faillock clears on reboot. The bundled
|
|
// "caelestia" service (assets/pam.d/caelestia) uses plain pam_unix.
|
|
config: "caelestia"
|
|
configDirectory: Quickshell.shellDir + "/assets/pam.d"
|
|
|
|
onMessageChanged: {
|
|
if (message.startsWith("The account is locked"))
|
|
root.lockMessage = message;
|
|
else if (root.lockMessage && message.endsWith(" left to unlock)"))
|
|
root.lockMessage += "\n" + message;
|
|
}
|
|
|
|
onResponseRequiredChanged: {
|
|
if (!responseRequired)
|
|
return;
|
|
|
|
respond(root.buffer);
|
|
root.buffer = "";
|
|
}
|
|
|
|
onCompleted: res => {
|
|
if (res === PamResult.Success)
|
|
return root.lock.unlock();
|
|
|
|
if (res === PamResult.Error)
|
|
root.state = "error";
|
|
else if (res === PamResult.MaxTries)
|
|
root.state = "max";
|
|
else if (res === PamResult.Failed)
|
|
root.state = "fail";
|
|
|
|
root.flashMsg();
|
|
stateReset.restart();
|
|
}
|
|
}
|
|
|
|
PamContext {
|
|
id: fprint
|
|
|
|
property bool available
|
|
property int tries
|
|
property int errorTries
|
|
|
|
function checkAvail(): void {
|
|
if (!available || !GlobalConfig.lock.enableFprint || !root.lock.secure) {
|
|
abort();
|
|
return;
|
|
}
|
|
|
|
tries = 0;
|
|
errorTries = 0;
|
|
start();
|
|
}
|
|
|
|
config: "fprint"
|
|
configDirectory: Quickshell.shellDir + "/assets/pam.d"
|
|
|
|
onCompleted: res => {
|
|
if (!available)
|
|
return;
|
|
|
|
if (res === PamResult.Success)
|
|
return root.lock.unlock();
|
|
|
|
if (res === PamResult.Error) {
|
|
root.fprintState = "error";
|
|
errorTries++;
|
|
if (errorTries < 5) {
|
|
abort();
|
|
errorRetry.restart();
|
|
}
|
|
} else if (res === PamResult.MaxTries) {
|
|
// Isn't actually the real max tries as pam only reports completed
|
|
// when max tries is reached.
|
|
tries++;
|
|
if (tries < GlobalConfig.lock.maxFprintTries) {
|
|
// Restart if not actually real max tries
|
|
root.fprintState = "fail";
|
|
start();
|
|
} else {
|
|
root.fprintState = "max";
|
|
abort();
|
|
}
|
|
}
|
|
|
|
root.flashMsg();
|
|
fprintStateReset.start();
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: availProc
|
|
|
|
command: ["sh", "-c", "fprintd-list $USER"]
|
|
onExited: code => { // qmllint disable signal-handler-parameters
|
|
fprint.available = code === 0;
|
|
fprint.checkAvail();
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: errorRetry
|
|
|
|
interval: 800
|
|
onTriggered: fprint.start()
|
|
}
|
|
|
|
Timer {
|
|
id: stateReset
|
|
|
|
interval: 4000
|
|
onTriggered: {
|
|
if (root.state !== "max")
|
|
root.state = "";
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: fprintStateReset
|
|
|
|
interval: 4000
|
|
onTriggered: {
|
|
root.fprintState = "";
|
|
fprint.errorTries = 0;
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
function onSecureChanged(): void {
|
|
if (root.lock.secure) {
|
|
availProc.running = true;
|
|
root.buffer = "";
|
|
root.state = "";
|
|
root.fprintState = "";
|
|
root.lockMessage = "";
|
|
}
|
|
}
|
|
|
|
function onUnlock(): void {
|
|
fprint.abort();
|
|
}
|
|
|
|
target: root.lock
|
|
}
|
|
|
|
Connections {
|
|
function onEnableFprintChanged(): void {
|
|
fprint.checkAvail();
|
|
}
|
|
|
|
target: GlobalConfig.lock
|
|
}
|
|
}
|