colour -> color, behaviour -> behavior, centre -> center, favourite -> favorite, and about twenty other pairs, applied consistently across comments, docs, error/UI copy, and a handful of QML identifiers that used the British spelling as their actual name: SystemSettings' serialiseValue/serialiseTable/normaliseGradient, Displays' normaliseModes, Wallpaper's normalisePolicy, SettingsBackup's serialiseHomeState, DateTime's ntpSynchronised property, Clipboard's _normalise helper, and ShortcutCapture's cancelled signal (with its onCancelled handler in ShortcutsPage.qml). Every call site and the two tests that assert on the literal source text (settings-ownership and settings-backup-live contracts) were updated in lockstep. Left untouched: config/dot/espanso/match/packages/misspell-en/ is a vendored third-party autocorrect dictionary -- its entries are typo corrections, not our prose, and rewriting them would fight the package's own purpose (and any future re-sync from upstream). The already-American `favorites` property (Home page pinned accessories) was never actually misspelled -- only nearby comments and error strings said "favourites" -- so no data migration was needed there. Claude-Session: https://claude.ai/code/session_01E6TJUAh41HaP25MVHWkhRZ
459 lines
17 KiB
QML
459 lines
17 KiB
QML
import QtQuick
|
|
import qs.config
|
|
import qs.services
|
|
|
|
SettingsPage {
|
|
id: root
|
|
|
|
objectName: "system-health-page"
|
|
|
|
title: "System Health"
|
|
lede: "Checks the parts of the desktop this app owns, and explains what needs attention."
|
|
|
|
property var pendingConfirmation: null
|
|
property string instructionTarget: ""
|
|
|
|
readonly property var issueChecks: Health.checks.filter(check => check.status === "warning" || check.status === "error")
|
|
readonly property var groups: [
|
|
{
|
|
group: "desktop-foundation",
|
|
title: "Desktop foundation",
|
|
subtitle: "Compositor, shell, portals, wallpaper, idle policy, and launcher."
|
|
},
|
|
{
|
|
group: "input-media",
|
|
title: "Input & media",
|
|
subtitle: "Sound, clipboard, capture, OCR, and display controls."
|
|
},
|
|
{
|
|
group: "integrations",
|
|
title: "Integrations",
|
|
subtitle: "Only configured integrations affect health."
|
|
},
|
|
{
|
|
group: "panama-tools",
|
|
title: "Desktop tools",
|
|
subtitle: "Tracked links, launcher commands, apps, and inhibitors."
|
|
}
|
|
]
|
|
readonly property var applicationTargets: ({
|
|
"integration.nextcloud": "nextcloud",
|
|
"integration.rustdesk": "rustdesk",
|
|
"integration.kdeconnect": "kdeconnect",
|
|
"integration.bluebubbles": "bluebubbles"
|
|
})
|
|
|
|
function statusLabel(status: string): string {
|
|
if (status === "ok") return "Healthy";
|
|
if (status === "warning") return "Needs attention";
|
|
if (status === "error") return "Action required";
|
|
return "Not set up";
|
|
}
|
|
|
|
function checksForGroup(group: string): var {
|
|
return Health.checks.filter(check => check.group === group
|
|
&& (check.status === "ok" || check.status === "unconfigured"));
|
|
}
|
|
|
|
function handleAction(check: var): void {
|
|
if (!check || !check.action)
|
|
return;
|
|
|
|
if (check.action.kind === "open") {
|
|
if (check.action.target) {
|
|
ShellState.openSettings(check.action.target);
|
|
return;
|
|
}
|
|
const application = root.applicationTargets[check.id];
|
|
if (application)
|
|
SystemSettings.openApplication(application);
|
|
return;
|
|
}
|
|
|
|
if (check.action.kind === "instructions") {
|
|
root.instructionTarget = check.action.target || "";
|
|
return;
|
|
}
|
|
|
|
if (check.action.kind !== "repair")
|
|
return;
|
|
if (check.action.confirm) {
|
|
root.pendingConfirmation = check;
|
|
return;
|
|
}
|
|
Health.repair(check.id, false);
|
|
}
|
|
|
|
function confirmRepair(): void {
|
|
const check = root.pendingConfirmation;
|
|
root.pendingConfirmation = null;
|
|
if (check)
|
|
Health.repair(check.id, false);
|
|
}
|
|
|
|
function descendants(item: var, prefix: string): var {
|
|
let matches = [];
|
|
if (!item)
|
|
return matches;
|
|
if (String(item.objectName || "").indexOf(prefix) === 0)
|
|
matches.push(item);
|
|
for (const child of item.children || [])
|
|
matches = matches.concat(root.descendants(child, prefix));
|
|
return matches;
|
|
}
|
|
|
|
function activateRenderedAction(id: string): bool {
|
|
const suffix = `:${id}`;
|
|
const rows = root.descendants(root, "health-check-row:").filter(row =>
|
|
row.visible && String(row.objectName).endsWith(suffix));
|
|
return rows.length === 1 && rows[0].activateAction();
|
|
}
|
|
|
|
function renderedFocusChain(): var {
|
|
// Repeater delegates enter Qt's tab chain lazily after their enabled
|
|
// binding changes at the end of a scan. Touch each rendered action's
|
|
// real next-focus link before traversing from the first hero control.
|
|
const renderedActions = root.descendants(root, "health-row-action:").filter(item =>
|
|
item.visible && item.enabled && item.activeFocusOnTab);
|
|
for (const action of renderedActions)
|
|
action.nextItemInFocusChain(true);
|
|
|
|
const starts = root.descendants(root, "health-copy-report-button").filter(item => item.visible && item.enabled);
|
|
if (starts.length !== 1)
|
|
return [];
|
|
|
|
const names = [];
|
|
const start = starts[0];
|
|
let current = start;
|
|
for (let index = 0; index < 128; index++) {
|
|
const name = String(current.objectName || "");
|
|
if (name !== "" && names.indexOf(name) < 0)
|
|
names.push(name);
|
|
current = current.nextItemInFocusChain(true);
|
|
if (!current || current === start)
|
|
break;
|
|
}
|
|
return names;
|
|
}
|
|
|
|
// Deterministic, read-only fixture seam used by the offscreen contract.
|
|
function uiDiagnostics(): var {
|
|
const summaries = root.descendants(root, "health-summary");
|
|
const rows = root.descendants(root, "health-check-row:").filter(row => row.visible);
|
|
const checkingLabels = root.descendants(root, "health-checking-label").filter(label => label.visible);
|
|
const confirmationSheets = root.descendants(root, "health-confirmation-sheet:").filter(sheet => sheet.visible);
|
|
const emptyGroups = root.descendants(root, "health-empty-group:").filter(label => label.visible);
|
|
const fedoraHandoffs = root.descendants(root, "health-fedora-handoff:").filter(row => row.visible);
|
|
return {
|
|
renderedRows: rows.map(row => {
|
|
const objectName = String(row.objectName);
|
|
const parts = objectName.split(":");
|
|
const statusTexts = root.descendants(row, "health-status-text:").filter(text => text.visible);
|
|
return {
|
|
objectName: objectName,
|
|
id: parts.slice(2).join(":"),
|
|
section: parts[1],
|
|
statusText: statusTexts.length === 1 ? statusTexts[0].text : ""
|
|
};
|
|
}),
|
|
summaryHeight: summaries.length > 0 ? summaries[0].height : 0,
|
|
rowHeights: rows.map(row => row.height),
|
|
checking: Health.busy,
|
|
checkingText: checkingLabels.length > 0 ? checkingLabels[0].text : "",
|
|
focusChain: root.renderedFocusChain(),
|
|
activatedRows: rows.filter(row => row.actionActivationCount > 0).map(row => String(row.objectName)),
|
|
emptyQuietGroups: emptyGroups.map(label => String(label.objectName).slice("health-empty-group:".length)),
|
|
fedoraHandoffs: fedoraHandoffs.map(row => ({
|
|
id: String(row.objectName).slice("health-fedora-handoff:".length),
|
|
label: row.label,
|
|
action: row.action
|
|
})),
|
|
confirmationVisible: confirmationSheets.length === 1,
|
|
confirmationId: confirmationSheets.length === 1
|
|
? String(confirmationSheets[0].objectName).slice("health-confirmation-sheet:".length)
|
|
: ""
|
|
};
|
|
}
|
|
|
|
Component.onCompleted: Health.refresh()
|
|
|
|
header: Component {
|
|
Rectangle {
|
|
objectName: `health-confirmation-sheet:${root.pendingConfirmation ? root.pendingConfirmation.id : ""}`
|
|
visible: root.pendingConfirmation !== null
|
|
implicitHeight: visible ? confirmRow.implicitHeight + 28 : 0
|
|
radius: Theme.cardRadius
|
|
color: Theme.mix(Theme.bgPanel, Theme.warn, 0.1)
|
|
border.width: 1
|
|
border.color: Theme.alpha(Theme.warn, 0.34)
|
|
|
|
Row {
|
|
id: confirmRow
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.margins: 16
|
|
spacing: 14
|
|
|
|
Column {
|
|
width: parent.width - cancelButton.width - repairButton.width - 28
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 3
|
|
|
|
Text {
|
|
width: parent.width
|
|
text: root.pendingConfirmation
|
|
? `${root.pendingConfirmation.action.label}?`
|
|
: "Restart the shell?"
|
|
color: Theme.fg
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSize
|
|
font.weight: Font.DemiBold
|
|
}
|
|
|
|
Text {
|
|
width: parent.width
|
|
text: "The desktop chrome will disappear briefly and return when Quickshell restarts."
|
|
color: Theme.fgDim
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
}
|
|
|
|
SettingsButton {
|
|
id: cancelButton
|
|
text: "Cancel"
|
|
activeFocusOnTab: true
|
|
border.width: activeFocus ? 2 : 1
|
|
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
|
onClicked: root.pendingConfirmation = null
|
|
Keys.onReturnPressed: root.pendingConfirmation = null
|
|
Keys.onSpacePressed: root.pendingConfirmation = null
|
|
}
|
|
|
|
SettingsButton {
|
|
id: repairButton
|
|
text: root.pendingConfirmation ? root.pendingConfirmation.action.label : "Restart the shell"
|
|
activeFocusOnTab: true
|
|
border.width: activeFocus ? 2 : 1
|
|
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
|
onClicked: root.confirmRepair()
|
|
Keys.onReturnPressed: root.confirmRepair()
|
|
Keys.onSpacePressed: root.confirmRepair()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
HealthSummary {}
|
|
|
|
SettingsCard {
|
|
visible: root.issueChecks.length > 0
|
|
title: Health.status === "error" ? "Action required" : "Needs attention"
|
|
subtitle: Health.status === "error"
|
|
? "Resolve these items first. Healthy systems remain listed below."
|
|
: "Nothing here prevents you from using the desktop."
|
|
|
|
Item {
|
|
width: parent.width
|
|
implicitHeight: issueRows.implicitHeight
|
|
|
|
Rectangle {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 4
|
|
anchors.top: parent.top
|
|
anchors.topMargin: 25
|
|
anchors.bottom: parent.bottom
|
|
anchors.bottomMargin: 25
|
|
width: 1
|
|
visible: root.issueChecks.length > 1
|
|
color: Theme.alpha(Health.status === "error" ? Theme.danger : Theme.warn, 0.44)
|
|
}
|
|
|
|
Column {
|
|
id: issueRows
|
|
width: parent.width
|
|
|
|
Repeater {
|
|
id: issueRepeater
|
|
model: root.issueChecks
|
|
|
|
HealthCheckRow {
|
|
required property var modelData
|
|
required property int index
|
|
|
|
width: issueRows.width
|
|
check: modelData
|
|
issue: true
|
|
divider: index < issueRepeater.count - 1
|
|
onActionRequested: check => root.handleAction(check)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
visible: root.instructionTarget === "ddc-permissions"
|
|
title: "External monitor brightness"
|
|
subtitle: "The monitor reports DDC/CI support, but this session cannot reach the monitor bus."
|
|
|
|
Item {
|
|
width: parent.width
|
|
implicitHeight: Math.max(instructionCopy.implicitHeight, doneButton.implicitHeight) + 8
|
|
|
|
Text {
|
|
id: instructionCopy
|
|
anchors.left: parent.left
|
|
anchors.right: doneButton.left
|
|
anchors.rightMargin: 18
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: "Reload the installed udev rules and trigger the i2c-dev and DRM devices. Sign out and back in if monitor access is still unavailable."
|
|
color: Theme.fgDim
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
|
|
SettingsButton {
|
|
id: doneButton
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: "Done"
|
|
activeFocusOnTab: true
|
|
border.width: activeFocus ? 2 : 1
|
|
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
|
onClicked: root.instructionTarget = ""
|
|
Keys.onReturnPressed: root.instructionTarget = ""
|
|
Keys.onSpacePressed: root.instructionTarget = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
Grid {
|
|
id: groupGrid
|
|
|
|
width: parent.width
|
|
columns: width >= 700 ? 2 : 1
|
|
columnSpacing: 16
|
|
rowSpacing: 16
|
|
|
|
Repeater {
|
|
model: root.groups
|
|
|
|
SettingsCard {
|
|
required property var modelData
|
|
readonly property var quietChecks: root.checksForGroup(modelData.group)
|
|
|
|
width: groupGrid.columns === 2
|
|
? (groupGrid.width - groupGrid.columnSpacing) / 2
|
|
: groupGrid.width
|
|
title: modelData.title
|
|
subtitle: modelData.subtitle
|
|
|
|
Column {
|
|
id: groupRows
|
|
width: parent.width
|
|
|
|
Text {
|
|
objectName: `health-empty-group:${modelData.group}`
|
|
width: parent.width
|
|
visible: quietChecks.length === 0
|
|
text: "Items needing attention are listed above."
|
|
color: Theme.fgMuted
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
topPadding: 8
|
|
bottomPadding: 8
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
|
|
Repeater {
|
|
id: groupRepeater
|
|
model: quietChecks
|
|
|
|
HealthCheckRow {
|
|
required property var modelData
|
|
required property int index
|
|
|
|
width: groupRows.width
|
|
check: modelData
|
|
divider: index < groupRepeater.count - 1
|
|
onActionRequested: check => root.handleAction(check)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Fedora system settings"
|
|
subtitle: "These areas remain owned by Fedora and GNOME's mature system panels."
|
|
|
|
Item {
|
|
width: parent.width
|
|
implicitHeight: 42
|
|
|
|
Text {
|
|
anchors.left: parent.left
|
|
anchors.right: gnomeSettingsButton.left
|
|
anchors.rightMargin: 18
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: "Use GNOME Settings for the parts of the system this app does not manage."
|
|
color: Theme.fgDim
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
|
|
SettingsButton {
|
|
id: gnomeSettingsButton
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: "Open GNOME Settings"
|
|
activeFocusOnTab: true
|
|
border.width: activeFocus ? 2 : 1
|
|
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
|
onClicked: SystemSettings.openGnomePanel("network")
|
|
Keys.onReturnPressed: SystemSettings.openGnomePanel("network")
|
|
Keys.onSpacePressed: SystemSettings.openGnomePanel("network")
|
|
}
|
|
}
|
|
|
|
ActionRow {
|
|
objectName: "health-fedora-handoff:users"
|
|
label: "Users"
|
|
detail: "Accounts, passwords, and automatic login"
|
|
action: "Open users"
|
|
onTriggered: SystemSettings.openGnomePanel("system", "users")
|
|
}
|
|
|
|
ActionRow {
|
|
objectName: "health-fedora-handoff:sharing"
|
|
label: "Sharing"
|
|
detail: "Remote desktop, media sharing, and remote login"
|
|
action: "Open sharing"
|
|
onTriggered: SystemSettings.openGnomePanel("sharing")
|
|
}
|
|
|
|
ActionRow {
|
|
objectName: "health-fedora-handoff:color"
|
|
label: "Color profiles"
|
|
detail: "Assigning an ICC profile here has no visible effect in this session: the colord daemon that loads it onto a display isn't running under Hyprland"
|
|
action: "Open color"
|
|
onTriggered: SystemSettings.openGnomePanel("color")
|
|
}
|
|
|
|
ActionRow {
|
|
objectName: "health-fedora-handoff:wellbeing"
|
|
label: "Digital wellbeing"
|
|
detail: "Screen time and break reminders"
|
|
action: "Open wellbeing"
|
|
divider: false
|
|
onTriggered: SystemSettings.openGnomePanel("wellbeing")
|
|
}
|
|
}
|
|
}
|