Files
Panama/config/dot/quickshell/modules/settings/AboutPage.qml
T

367 lines
13 KiB
QML

// About.
//
// The landing page of the System category, and the answer to "what am I
// running". Every line here is read from the machine rather than written down:
// the versions block used to carry a hardcoded Quickshell version that had been
// wrong for two releases, and a Design principles card restating opinions the
// manual argues properly.
//
// Rows come from MachineInfo, except graphics, which is joined from
// GraphicsDevices rather than read a second time -- two readouts of the same
// hardware are two things that can disagree. Facts the cards below do not claim
// by name are still shown, in Software, so a row the helper learns to report
// cannot go missing here.
import Quickshell
import Quickshell.Io
import QtQuick
import qs.config
import qs.services
SettingsPage {
id: root
objectName: "about"
title: "About"
lede: "This machine, plainly."
Component.onCompleted: {
if (!MachineInfo.scanned)
MachineInfo.refresh();
if (GraphicsDevices.devices.length === 0)
GraphicsDevices.refresh();
}
// ── Reading the machine ──────────────────────────────────────────────────
function fact(label: string): string {
const row = (MachineInfo.facts ?? []).find(entry => entry.label === label);
return row ? String(row.value ?? "") : "";
}
// Joined with a middle dot, skipping whatever is absent. Firmware and
// Secure Boot are both absent-tolerant on the helper's side, so either half
// of that row can be missing on a given machine.
function joined(parts: var): string {
return parts.filter(part => String(part ?? "") !== "").join(" · ");
}
readonly property string hostname: root.fact("Hostname")
// "Fedora Linux 44 (Workstation Edition)" says the edition twice for the
// one line where brevity matters most.
readonly property string operatingSystem:
root.fact("Operating system").replace(/\s*\(.*\)\s*$/, "")
readonly property string uptime: root.fact("Uptime").split(",")[0].trim()
readonly property string identity: root.joined([
root.operatingSystem === "" ? "" : "Panama on " + root.operatingSystem,
SystemSettings.hyprlandVersion === "" ? "" : "Hyprland " + SystemSettings.hyprlandVersion,
root.uptime === "" ? "" : "up " + root.uptime
])
readonly property var graphicsRows: {
const gpus = GraphicsDevices.devices ?? [];
return gpus.map((device, index) => ({
label: gpus.length > 1 ? "Graphics " + (index + 1) : "Graphics",
value: String(device.name ?? "")
}));
}
readonly property string displayValue: root.joined([
SystemSettings.monitorName, root.fact("Resolution")
])
// Everything the three cards below name explicitly. Whatever the helper
// reports that is not in here lands in Software rather than nowhere.
readonly property var claimedFacts: [
"Hostname", "Operating system", "Uptime", "Panama", "Kernel",
"Firmware", "Secure Boot", "Model", "Processor", "Memory", "Swap",
"Disk", "Resolution"
]
readonly property var otherFacts: (MachineInfo.facts ?? [])
.filter(entry => root.claimedFacts.indexOf(String(entry.label ?? "")) < 0)
// ── Copying it ───────────────────────────────────────────────────────────
// The same facts, as plain text, for a bug report or a message to somebody
// trying to help. Built from what is on screen so the two cannot disagree.
function systemInfoText(): string {
const lines = [];
if (root.hostname !== "")
lines.push(root.hostname);
if (root.identity !== "")
lines.push(root.identity);
lines.push("");
for (const row of root.versionRows.concat(root.hardwareRows, root.otherFacts)) {
if (String(row.value ?? "") !== "")
lines.push(row.label + ": " + row.value);
}
return lines.join("\n") + "\n";
}
property string copyResult: ""
Process {
id: copyRun
property string payload: ""
command: ["wl-copy"]
stdinEnabled: true
onStarted: {
copyRun.write(copyRun.payload);
// wl-copy reads stdin until EOF before it exits; leaving the
// channel open would hang it forever waiting for more. Same
// stdinEnabled close that Health.copyReport uses.
copyRun.stdinEnabled = false;
}
onExited: exitCode => {
root.copyResult = exitCode === 0
? "Copied."
: "Could not reach the clipboard.";
copyRun.payload = "";
}
}
function copySystemInfo(): void {
if (copyRun.running)
return;
root.copyResult = "";
copyRun.payload = root.systemInfoText();
copyRun.stdinEnabled = true;
copyRun.running = true;
}
// ── The hero ─────────────────────────────────────────────────────────────
readonly property var versionRows: [
{ label: "Panama", value: root.fact("Panama") },
{ label: "Quickshell", value: SystemSettings.quickshellVersion },
{ label: "Kernel", value: root.fact("Kernel") },
{ label: "Firmware", value: root.joined([
root.fact("Firmware"),
root.fact("Secure Boot") === "" ? "" : "Secure Boot " + root.fact("Secure Boot")
]) }
].filter(row => String(row.value ?? "") !== "")
SettingsCard {
Item {
width: parent.width
implicitHeight: Math.max(96, heroCopy.implicitHeight + 16)
// The house tile: a rounded plate carrying the System glyph the
// sidebar uses, in the accent, drawn rather than shipped as an
// image so it follows the theme like everything else.
Rectangle {
id: heroArt
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 84
height: 84
radius: 24
color: Theme.alpha(Theme.accent, 0.13)
border.width: 1
border.color: Theme.alpha(Theme.accent, 0.3)
Rectangle {
anchors.fill: parent
radius: parent.radius
color: "transparent"
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop { position: 0; color: Theme.alpha(Theme.accentSecondary, 0.14) }
GradientStop { position: 1; color: Theme.alpha(Theme.accent, 0.02) }
}
}
Text {
anchors.centerIn: parent
text: "\u{F02FD}"
color: Theme.accent
font.family: Theme.fontMono
font.pixelSize: 38
}
}
Column {
id: heroCopy
anchors.left: heroArt.right
anchors.leftMargin: 20
anchors.right: copyButton.left
anchors.rightMargin: 18
anchors.verticalCenter: parent.verticalCenter
spacing: 5
Text {
width: parent.width
text: root.hostname === "" ? "This machine" : root.hostname
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeTitle
font.weight: Font.Bold
elide: Text.ElideRight
}
Text {
width: parent.width
visible: root.identity !== ""
text: root.identity
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.WordWrap
}
Text {
width: parent.width
visible: root.copyResult !== ""
text: root.copyResult
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}
SettingsButton {
id: copyButton
objectName: "about-copy-button"
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: "Copy system info"
enabled: !copyRun.running
activeFocusOnTab: enabled
border.width: activeFocus ? 2 : 1
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
onClicked: root.copySystemInfo()
Accessible.role: Accessible.Button
Accessible.name: "Copy system info"
Keys.onReturnPressed: root.copySystemInfo()
Keys.onSpacePressed: root.copySystemInfo()
}
}
Repeater {
model: root.versionRows
TextRow {
required property var modelData
required property int index
label: modelData.label
value: modelData.value
divider: index < root.versionRows.length - 1
}
}
}
// ── Hardware ─────────────────────────────────────────────────────────────
readonly property var hardwareRows: [
{ label: "Model", value: root.fact("Model") },
{ label: "Processor", value: root.fact("Processor") }
].concat(root.graphicsRows, [
{ label: "Memory", value: root.joined([
root.fact("Memory"),
root.fact("Swap") === "" ? "" : root.fact("Swap") + " swap"
]) },
{ label: "Disk", value: root.fact("Disk") },
{ label: "Display", value: root.displayValue }
]).filter(row => String(row.value ?? "") !== "")
SettingsCard {
title: "Hardware"
Repeater {
model: root.hardwareRows
TextRow {
required property var modelData
label: modelData.label
value: modelData.value
}
}
TextRow {
visible: MachineInfo.scanned && root.hardwareRows.length === 0
label: "Hardware"
detail: "The system did not report anything readable"
value: "Unavailable"
}
// The machine's name is a network-facing setting rather than a fact
// about the hardware, and Sharing is the page that owns it. Saying so
// here is cheaper than a second field that writes the same hostname.
ActionRow {
label: "Device name"
detail: root.hostname === ""
? "What this machine calls itself on the network"
: root.hostname + " — what this machine calls itself on the network"
action: "Open Sharing"
divider: false
onTriggered: ShellState.openSettings("sharing")
}
}
// ── Software ─────────────────────────────────────────────────────────────
SettingsCard {
title: "Software"
Repeater {
model: root.otherFacts
TextRow {
required property var modelData
label: String(modelData.label ?? "")
value: String(modelData.value ?? "")
}
}
TextRow {
label: "Configuration"
detail: Quickshell.shellDir
value: "Local"
divider: false
}
}
// ── Manual ───────────────────────────────────────────────────────────────
ManualChapters { id: contents }
SettingsCard {
title: "Manual"
subtitle: "How this desktop works, written for the person using it. Links inside a chapter open the settings page they name."
Repeater {
id: chapterRows
model: contents.titled
ActionRow {
required property var modelData
required property int index
label: modelData.title
action: "Read"
onTriggered: ShellState.openSettingsSection("manual", modelData.file)
}
}
ActionRow {
label: "Open the manual"
detail: "Starts at the beginning"
action: "Open"
divider: false
onTriggered: ShellState.openSettings("manual")
}
}
}