nosignal-shell/modules/lock/Pam.qml
28allday 152923f5d8 fold: bake NoSignal shell patches into the fork (Phase 2)
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>
2026-06-13 22:08:59 +01:00

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
}
}