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>
This commit is contained in:
parent
71288e2126
commit
152923f5d8
9 changed files with 395 additions and 6 deletions
20
assets/pam.d/caelestia
Normal file
20
assets/pam.d/caelestia
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
#%PAM-1.0
|
||||||
|
# Caelestia lock-screen authentication (NoSignal).
|
||||||
|
#
|
||||||
|
# Deliberately does NOT use pam_faillock: a desktop screen-lock must never lock
|
||||||
|
# the user out of their OWN running session. With faillock in the path (the old
|
||||||
|
# "passwd"->system-auth route), a few failed unlocks tripped a temporary account
|
||||||
|
# lock that then refused the *correct* password until /run/faillock cleared on
|
||||||
|
# reboot. See README.md / finding F1.
|
||||||
|
#
|
||||||
|
# pam_unix verifies the password supplied via the PAM conversation (the lock UI)
|
||||||
|
# using the setuid unix_chkpwd helper, so it works for the uid-1000 Quickshell
|
||||||
|
# process. Add pam_systemd_home below if/when systemd-homed users are supported.
|
||||||
|
#
|
||||||
|
# F8a: deliberately NO `nullok` — a screen lock must never accept an empty
|
||||||
|
# password. (Only changes behaviour for empty-password accounts, which are
|
||||||
|
# correctly rejected; normal password auth is unaffected.)
|
||||||
|
auth required pam_unix.so
|
||||||
|
account required pam_unix.so
|
||||||
|
password required pam_unix.so
|
||||||
|
session required pam_unix.so
|
||||||
|
|
@ -40,7 +40,12 @@ Scope {
|
||||||
PamContext {
|
PamContext {
|
||||||
id: passwd
|
id: passwd
|
||||||
|
|
||||||
config: "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"
|
configDirectory: Quickshell.shellDir + "/assets/pam.d"
|
||||||
|
|
||||||
onMessageChanged: {
|
onMessageChanged: {
|
||||||
|
|
|
||||||
|
|
@ -73,10 +73,20 @@ QtObject {
|
||||||
|
|
||||||
// System
|
// System
|
||||||
Component {
|
Component {
|
||||||
PlaceholderComp {}
|
// Updates (NoSignal)
|
||||||
|
StackPage {
|
||||||
|
Component {
|
||||||
|
UpdatesPage {}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Component {
|
Component {
|
||||||
PlaceholderComp {}
|
// Additions (NoSignal)
|
||||||
|
StackPage {
|
||||||
|
Component {
|
||||||
|
AdditionsPage {}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Shell
|
// Shell
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,9 @@ QtObject {
|
||||||
category: "system"
|
category: "system"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: qsTr("Plugins"),
|
label: qsTr("Additions"),
|
||||||
icon: "extension",
|
icon: "extension",
|
||||||
description: qsTr("Manage plugins"),
|
description: qsTr("Install optional software"),
|
||||||
category: "system"
|
category: "system"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
69
modules/nexus/common/SudoToggleRow.qml
Normal file
69
modules/nexus/common/SudoToggleRow.qml
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
// SudoToggleRow.qml (NoSignal) — Settings -> Services toggle for time-boxed
|
||||||
|
// passwordless sudo. Reflects live state by polling `nosignal-sudo-toggle
|
||||||
|
// status` (no root needed); enabling opens a floating terminal for the ONE
|
||||||
|
// password prompt, disabling runs passwordless inside the active window.
|
||||||
|
//
|
||||||
|
// Untracked file under modules/nexus/common — auto-discovered as the type
|
||||||
|
// `SudoToggleRow` via `import qs.modules.nexus.common`. Survives caelestia
|
||||||
|
// upgrades; only the one-line insert in ServicesPage.qml is re-applied by hook.
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.modules.nexus.common
|
||||||
|
|
||||||
|
ToggleRow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool active: false
|
||||||
|
property int remaining: 0
|
||||||
|
|
||||||
|
text: qsTr("Passwordless sudo (15 min)")
|
||||||
|
subtext: active
|
||||||
|
? qsTr("On — %1 min left. Auto-reverts; a reboot also clears it.").arg(remaining)
|
||||||
|
: qsTr("Run sudo without a password for 15 minutes, then it reverts")
|
||||||
|
|
||||||
|
onToggled: {
|
||||||
|
if (checked)
|
||||||
|
enableProc.running = true; // needs a password -> floating terminal
|
||||||
|
else
|
||||||
|
disableProc.running = true; // no password inside the active window
|
||||||
|
reconcile.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- live state -----------------------------------------------------------
|
||||||
|
Process {
|
||||||
|
id: statusProc
|
||||||
|
command: ["nosignal-sudo-toggle", "status"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
const p = text.trim().split(/\s+/);
|
||||||
|
root.active = p[0] === "active";
|
||||||
|
root.remaining = parseInt(p[1] || "0") || 0;
|
||||||
|
root.checked = root.active; // drive switch from real state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- actions --------------------------------------------------------------
|
||||||
|
Process {
|
||||||
|
id: enableProc
|
||||||
|
command: ["kitty", "--class", "TUI.float", "-e", "sudo", "nosignal-sudo-toggle", "enable"]
|
||||||
|
}
|
||||||
|
Process {
|
||||||
|
id: disableProc
|
||||||
|
command: ["sudo", "-n", "nosignal-sudo-toggle", "disable"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- polling --------------------------------------------------------------
|
||||||
|
Timer {
|
||||||
|
interval: 5000
|
||||||
|
repeat: true
|
||||||
|
running: true
|
||||||
|
triggeredOnStart: true
|
||||||
|
onTriggered: statusProc.running = true
|
||||||
|
}
|
||||||
|
Timer {
|
||||||
|
id: reconcile
|
||||||
|
interval: 2000
|
||||||
|
onTriggered: statusProc.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
120
modules/nexus/pages/AdditionsPage.qml
Normal file
120
modules/nexus/pages/AdditionsPage.qml
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell.Io
|
||||||
|
import Caelestia.Config
|
||||||
|
import qs.components
|
||||||
|
import qs.services
|
||||||
|
import qs.modules.nexus.common
|
||||||
|
|
||||||
|
// NoSignal: the Additions settings page (replaces the upstream Plugins
|
||||||
|
// placeholder). Optional software installed on demand from official sources
|
||||||
|
// (pacman repos / upstream installers — no AUR, no Flatpak). Items come from
|
||||||
|
// the additions.json manifest via the status cache written by
|
||||||
|
// nosignal-additions; Install runs in a visible floating terminal so
|
||||||
|
// git/sudo/pacman output and prompts stay in front of the user.
|
||||||
|
PageBase {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
title: qsTr("Additions")
|
||||||
|
|
||||||
|
property var status: ({})
|
||||||
|
readonly property var items: status.items || []
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.top: parent.top
|
||||||
|
width: root.cappedWidth
|
||||||
|
spacing: Tokens.spacing.extraSmall / 2
|
||||||
|
|
||||||
|
// The Process objects live INSIDE the layout (its `data` accepts
|
||||||
|
// non-visual objects) — PageBase's default property is a single
|
||||||
|
// `Item`, so declaring them at page level kills the whole shell.
|
||||||
|
// Same pattern as UpdatesPage / the upstream AboutPage.
|
||||||
|
|
||||||
|
// Read the cached status (instant).
|
||||||
|
Process {
|
||||||
|
id: readProc
|
||||||
|
|
||||||
|
running: true
|
||||||
|
command: ["sh", "-c", "cat \"${XDG_STATE_HOME:-$HOME/.local/state}/nosignal/additions-status.json\" 2>/dev/null"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
try {
|
||||||
|
root.status = JSON.parse(text);
|
||||||
|
} catch (e) {
|
||||||
|
root.status = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the cache on demand (re-runs every item's check).
|
||||||
|
Process {
|
||||||
|
id: checkProc
|
||||||
|
|
||||||
|
command: ["sh", "-c", "\"$HOME/.local/bin/nosignal-additions\" status >/dev/null 2>&1"]
|
||||||
|
onExited: readProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run an installer in a visible floating terminal.
|
||||||
|
Process {
|
||||||
|
id: installProc
|
||||||
|
|
||||||
|
property string addId: ""
|
||||||
|
|
||||||
|
command: ["kitty", "--class", "TUI.float", "-e", "sh", "-c", "\"$HOME/.local/bin/nosignal-additions\" install " + addId + "; printf '\\nPress Enter to close...'; read _"]
|
||||||
|
onExited: readProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionHeader {
|
||||||
|
text: qsTr("Optional software")
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.items
|
||||||
|
|
||||||
|
NavRow {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
first: index === 0
|
||||||
|
last: index === root.items.length - 1
|
||||||
|
icon: modelData.icon || "extension"
|
||||||
|
label: modelData.name
|
||||||
|
status: modelData.installed ? qsTr("Installed") : modelData.desc
|
||||||
|
onClicked: {
|
||||||
|
if (!modelData.installed && !installProc.running) {
|
||||||
|
installProc.addId = modelData.id;
|
||||||
|
installProc.running = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoRow {
|
||||||
|
visible: root.items.length === 0
|
||||||
|
first: true
|
||||||
|
last: true
|
||||||
|
label: qsTr("No additions manifest")
|
||||||
|
value: "—"
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionHeader {
|
||||||
|
text: qsTr("Actions")
|
||||||
|
}
|
||||||
|
|
||||||
|
NavRow {
|
||||||
|
first: true
|
||||||
|
last: true
|
||||||
|
icon: "refresh"
|
||||||
|
label: qsTr("Re-check installed state")
|
||||||
|
status: installProc.running ? qsTr("Install running in terminal…") : (checkProc.running ? qsTr("Checking…") : qsTr("Refreshes the list above"))
|
||||||
|
onClicked: {
|
||||||
|
if (!checkProc.running)
|
||||||
|
checkProc.running = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -211,6 +211,11 @@ PageBase {
|
||||||
onToggled: GlobalConfig.services.smartScheme = checked
|
onToggled: GlobalConfig.services.smartScheme = checked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NoSignal: time-boxed passwordless sudo (15 min) toggle
|
||||||
|
SudoToggleRow {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
SelectRow {
|
SelectRow {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
last: true
|
last: true
|
||||||
|
|
|
||||||
139
modules/nexus/pages/UpdatesPage.qml
Normal file
139
modules/nexus/pages/UpdatesPage.qml
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell.Io
|
||||||
|
import Caelestia.Config
|
||||||
|
import qs.components
|
||||||
|
import qs.services
|
||||||
|
import qs.modules.nexus.common
|
||||||
|
|
||||||
|
// NoSignal: the Updates settings page (replaces the upstream placeholder).
|
||||||
|
// Reads the JSON cache written by nosignal-update-check; "Update now" runs
|
||||||
|
// nosignal-update in a floating terminal (sudo + pacman prompts stay visible).
|
||||||
|
PageBase {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
title: qsTr("Updates")
|
||||||
|
|
||||||
|
property var status: ({})
|
||||||
|
|
||||||
|
function count(v) {
|
||||||
|
return v === undefined ? "…" : String(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.top: parent.top
|
||||||
|
width: root.cappedWidth
|
||||||
|
spacing: Tokens.spacing.extraSmall / 2
|
||||||
|
|
||||||
|
// The Process objects live INSIDE the layout (its `data` accepts
|
||||||
|
// non-visual objects) — PageBase's default property is a single
|
||||||
|
// `Item`, so declaring them at page level kills the whole shell
|
||||||
|
// ("Cannot assign Process to QQuickItem*"). Same pattern as the
|
||||||
|
// upstream AboutPage.
|
||||||
|
|
||||||
|
// Read the cached status (instant; the user timer keeps it fresh).
|
||||||
|
Process {
|
||||||
|
id: readProc
|
||||||
|
|
||||||
|
running: true
|
||||||
|
command: ["sh", "-c", "cat \"${XDG_STATE_HOME:-$HOME/.local/state}/nosignal/update-status.json\" 2>/dev/null"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
try {
|
||||||
|
root.status = JSON.parse(text);
|
||||||
|
} catch (e) {
|
||||||
|
root.status = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the cache on demand.
|
||||||
|
Process {
|
||||||
|
id: checkProc
|
||||||
|
|
||||||
|
command: ["sh", "-c", "\"$HOME/.local/bin/nosignal-update-check\" >/dev/null 2>&1"]
|
||||||
|
onExited: readProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the real update in a visible floating terminal.
|
||||||
|
Process {
|
||||||
|
id: updateProc
|
||||||
|
|
||||||
|
command: ["kitty", "--class", "TUI.float", "-e", "sh", "-c", "\"$HOME/.local/bin/nosignal-update\"; \"$HOME/.local/bin/nosignal-update-check\" >/dev/null 2>&1; printf '\\nPress Enter to close...'; read _"]
|
||||||
|
onExited: readProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionHeader {
|
||||||
|
text: qsTr("Pending updates")
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoRow {
|
||||||
|
first: true
|
||||||
|
label: qsTr("Official packages")
|
||||||
|
value: root.count(root.status.repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoRow {
|
||||||
|
label: qsTr("AUR packages")
|
||||||
|
value: root.count(root.status.aur)
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoRow {
|
||||||
|
label: qsTr("Flatpak")
|
||||||
|
value: root.count(root.status.flatpak)
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoRow {
|
||||||
|
last: true
|
||||||
|
label: qsTr("NoSignal layer")
|
||||||
|
subtext: qsTr("Idempotent migrations applied by nosignal-update")
|
||||||
|
value: root.status.migrations_pending === undefined ? "…" : (root.status.migrations_pending > 0 ? qsTr("%1 migration(s) pending").arg(root.status.migrations_pending) : qsTr("up to date"))
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionHeader {
|
||||||
|
text: qsTr("History")
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoRow {
|
||||||
|
first: true
|
||||||
|
label: qsTr("Last full upgrade")
|
||||||
|
value: root.status.last_upgrade || "—"
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoRow {
|
||||||
|
last: true
|
||||||
|
label: qsTr("Last checked")
|
||||||
|
value: root.status.checked ? root.status.checked.replace("T", " ").substring(0, 16) : "—"
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionHeader {
|
||||||
|
text: qsTr("Actions")
|
||||||
|
}
|
||||||
|
|
||||||
|
NavRow {
|
||||||
|
first: true
|
||||||
|
icon: "refresh"
|
||||||
|
label: qsTr("Check for updates now")
|
||||||
|
status: checkProc.running ? qsTr("Checking…") : qsTr("Refreshes the counts above")
|
||||||
|
onClicked: {
|
||||||
|
if (!checkProc.running)
|
||||||
|
checkProc.running = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NavRow {
|
||||||
|
last: true
|
||||||
|
icon: "system_update_alt"
|
||||||
|
label: qsTr("Update now")
|
||||||
|
status: updateProc.running ? qsTr("Running in terminal…") : qsTr("Opens a terminal: snapshot, packages, NoSignal layer")
|
||||||
|
onClicked: {
|
||||||
|
if (!updateProc.running)
|
||||||
|
updateProc.running = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -70,7 +70,28 @@ QtObject {
|
||||||
const hasSavedProfile = Nmcli.hasSavedProfile(network.ssid);
|
const hasSavedProfile = Nmcli.hasSavedProfile(network.ssid);
|
||||||
|
|
||||||
if (hasSavedProfile) {
|
if (hasSavedProfile) {
|
||||||
Nmcli.connectToNetwork(network.ssid, "", network.bssid, null);
|
// NoSignal wifi-password-retry: a saved profile can hold a wrong
|
||||||
|
// password. Stock code passed a null callback here, so a failed
|
||||||
|
// activation never re-prompted — the dialog never reopened. On
|
||||||
|
// auth failure: forget the bad profile and reopen the dialog
|
||||||
|
// (same cleanup + dialog path the no-saved-profile branch uses).
|
||||||
|
Nmcli.connectToNetwork(network.ssid, "", network.bssid, result => {
|
||||||
|
if (result && result.needsPassword) {
|
||||||
|
if (Nmcli.pendingConnection) {
|
||||||
|
Nmcli.connectionCheckTimer.stop();
|
||||||
|
Nmcli.immediateCheckTimer.stop();
|
||||||
|
Nmcli.immediateCheckTimer.checkCount = 0;
|
||||||
|
Nmcli.pendingConnection = null;
|
||||||
|
}
|
||||||
|
Nmcli.forgetNetwork(network.ssid);
|
||||||
|
if (session && session.network) {
|
||||||
|
session.network.showPasswordDialog = true;
|
||||||
|
session.network.pendingNetwork = network;
|
||||||
|
} else if (onPasswordNeeded) {
|
||||||
|
onPasswordNeeded(network);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// Use password check with callback
|
// Use password check with callback
|
||||||
Nmcli.connectToNetworkWithPasswordCheck(network.ssid, network.isSecure, result => {
|
Nmcli.connectToNetworkWithPasswordCheck(network.ssid, network.isSecure, result => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue