docs/ is engineering artifacts -- design specs, plans, an upstream ledger -- and none of it is written for the person who has to live here. Five chapters that are: coming from another desktop, the keyboard, windows and workspaces, when something breaks, and making it yours. Rendered inside Settings rather than opened in a browser, so a chapter that says "the Displays page reverts after fifteen seconds" is one click from the Displays page. Qt's markdown renderer does the work; one chapter per Text, because Text has an implicit texture size limit and a document long enough to hit it goes blank rather than complaining. The chapters live beside the shell in manual/ rather than at the repository root, which departs from the plan. The reason is the path: the shell finds them through Quickshell.shellDir, which is correct wherever the repository is, whereas walking upward out of the shell directory is only correct by accident. The contract fails that pattern if it comes back. The contract also pins the set both ways -- every chapter listed exists, every chapter that exists is listed -- because a renamed file shows an error card where a chapter should be, which reads as a broken manual rather than as a moved file.
367 lines
14 KiB
QML
367 lines
14 KiB
QML
import QtQuick
|
|
import qs.config
|
|
import qs.services
|
|
|
|
Rectangle {
|
|
id: root
|
|
|
|
property string selectedPage: "home"
|
|
property string query: searchInput.text.trim().toLowerCase()
|
|
signal pageRequested(string page)
|
|
|
|
readonly property var results: SettingsSearch.search(root.query)
|
|
|
|
onQueryChanged: {
|
|
if (sidebarScroll)
|
|
sidebarScroll.contentY = 0;
|
|
}
|
|
|
|
function pageLabel(page: string): string {
|
|
const found = root.destinations.find(item => item.page === page);
|
|
return found ? found.label : "Settings";
|
|
}
|
|
|
|
readonly property var destinations: [
|
|
{ page: "home", label: "Home", icon: "\u{F02DC}" },
|
|
{ page: "appearance", label: "Appearance", icon: "\u{F0E0D}" },
|
|
{ page: "displays", label: "Displays", icon: "\u{F0379}" },
|
|
{ page: "connectivity", label: "Network & Devices", icon: "\u{F08D4}" },
|
|
{ page: "sharing", label: "Sharing", icon: "\u{F04E6}" },
|
|
{ page: "firewall", label: "Firewall", icon: "\u{F0483}" },
|
|
{ page: "printers", label: "Printers", icon: "\u{F042A}" },
|
|
{ page: "containers", label: "Containers", icon: "\u{F0868}" },
|
|
{ page: "ssh-keys", label: "SSH Keys", icon: "\u{F0306}" },
|
|
{ page: "home-phone", label: "Home & Phone", icon: "\u{F02DC}" },
|
|
{ page: "desktop", label: "Desktop & Dock", icon: "\u{F04A4}" },
|
|
{ page: "sound", label: "Sound", icon: "\u{F057E}" },
|
|
{ page: "gaming", label: "Gaming", icon: "\u{F0297}" },
|
|
{ page: "notifications", label: "Notifications & Focus", icon: "\u{F009A}" },
|
|
{ page: "screen-intelligence", label: "Screen Intelligence", icon: "\u{F05A8}" },
|
|
{ page: "shortcuts", label: "Keyboard", icon: "\u{F030C}" },
|
|
{ page: "mouse", label: "Mouse & Touchpad", icon: "\u{F037D}" },
|
|
{ page: "privacy", label: "Privacy & Security", icon: "\u{F0483}" },
|
|
{ page: "region", label: "Region & Language", icon: "\u{F0AC2}" },
|
|
{ page: "accounts", label: "Online Accounts", icon: "\u{F0004}" },
|
|
{ page: "accessibility", label: "Accessibility", icon: "\u{F0208}" },
|
|
{ page: "power", label: "Power & Lock", icon: "\u{F0425}" },
|
|
{ page: "datetime", label: "Date & Time", icon: "\u{F0954}" },
|
|
{ page: "applications", label: "Applications", icon: "\u{F003B}" },
|
|
{ page: "updates", label: "Software Update", icon: "\u{F06B0}" },
|
|
{ page: "storage", label: "Storage", icon: "\u{F02CA}" },
|
|
{ page: "snapshots", label: "Snapshots", icon: "\u{F0954}" },
|
|
{ page: "users", label: "Users", icon: "\u{F0004}" },
|
|
{ page: "services", label: "System Health", icon: "\u{F0493}" },
|
|
{ page: "manual", label: "Manual", icon: "\u{F02D4}" },
|
|
{ page: "about", label: "About", icon: "\u{F02FD}" }
|
|
]
|
|
|
|
width: 272
|
|
color: Theme.alpha(Theme.bgDark, 0.96)
|
|
border.width: 0
|
|
|
|
Column {
|
|
id: sidebarHeader
|
|
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.leftMargin: 18
|
|
anchors.rightMargin: 18
|
|
anchors.topMargin: 18
|
|
height: implicitHeight
|
|
spacing: 12
|
|
|
|
Rectangle {
|
|
width: parent.width
|
|
height: 36
|
|
radius: 10
|
|
color: Theme.alpha(Theme.fg, searchArea.containsMouse || searchInput.activeFocus ? 0.105 : 0.065)
|
|
border.width: 1
|
|
border.color: searchInput.activeFocus ? Theme.alpha(Theme.accent, 0.5) : Theme.alpha(Theme.fg, 0.055)
|
|
|
|
Text {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 11
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: "\u{F0349}"
|
|
color: Theme.fgDim
|
|
font.family: Theme.fontMono
|
|
font.pixelSize: 14
|
|
}
|
|
|
|
TextInput {
|
|
id: searchInput
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 35
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 10
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
color: Theme.fg
|
|
selectionColor: Theme.accent
|
|
selectedTextColor: Theme.bgDark
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSize
|
|
clip: true
|
|
|
|
Text {
|
|
anchors.fill: parent
|
|
visible: !searchInput.text && !searchInput.activeFocus
|
|
text: "Search settings"
|
|
color: Theme.fgMuted
|
|
font: searchInput.font
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: searchArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.IBeamCursor
|
|
onClicked: searchInput.forceActiveFocus()
|
|
}
|
|
}
|
|
}
|
|
|
|
Flickable {
|
|
id: sidebarScroll
|
|
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: sidebarHeader.bottom
|
|
anchors.bottom: healthFooter.top
|
|
anchors.leftMargin: 18
|
|
anchors.rightMargin: 18
|
|
anchors.topMargin: 12
|
|
anchors.bottomMargin: 12
|
|
contentWidth: width
|
|
contentHeight: scrollContent.implicitHeight
|
|
flickableDirection: Flickable.VerticalFlick
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
clip: true
|
|
|
|
Column {
|
|
id: scrollContent
|
|
|
|
width: sidebarScroll.width
|
|
|
|
// ── Search results ──────────────────────────────────────────────
|
|
// Typing searches the settings themselves, not page names.
|
|
Column {
|
|
id: searchResults
|
|
|
|
width: parent.width
|
|
spacing: 3
|
|
visible: root.query !== ""
|
|
|
|
Text {
|
|
width: parent.width
|
|
leftPadding: 4
|
|
bottomPadding: 4
|
|
text: root.results.length === 0
|
|
? "Nothing matches"
|
|
: root.results.length + (root.results.length === 1 ? " result" : " results")
|
|
color: Theme.fgMuted
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
}
|
|
|
|
Repeater {
|
|
model: root.results
|
|
|
|
Rectangle {
|
|
id: hit
|
|
|
|
required property var modelData
|
|
|
|
width: parent.width
|
|
height: 44
|
|
radius: 10
|
|
color: hitMouse.containsMouse ? Theme.alpha(Theme.fg, 0.08) : "transparent"
|
|
border.width: 0
|
|
|
|
Column {
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.leftMargin: 12
|
|
anchors.rightMargin: 10
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 1
|
|
|
|
Text {
|
|
width: parent.width
|
|
text: hit.modelData.label
|
|
color: Theme.fg
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSize
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
Text {
|
|
width: parent.width
|
|
text: hit.modelData.kind === "shortcut"
|
|
? hit.modelData.detail
|
|
: root.pageLabel(hit.modelData.page)
|
|
color: hit.modelData.kind === "shortcut" ? Theme.accent : Theme.fgMuted
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: hitMouse
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
root.pageRequested(hit.modelData.page);
|
|
searchInput.text = "";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
id: navigationList
|
|
|
|
width: parent.width
|
|
spacing: 4
|
|
visible: root.query === ""
|
|
|
|
Repeater {
|
|
model: root.destinations
|
|
|
|
Rectangle {
|
|
id: navItem
|
|
required property var modelData
|
|
width: parent.width
|
|
height: 40
|
|
radius: 10
|
|
color: modelData.page === root.selectedPage
|
|
? Theme.alpha(Theme.accent, 0.17)
|
|
: (navMouse.containsMouse ? Theme.alpha(Theme.fg, Theme.hoverAlpha * 0.55) : Theme.alpha(Theme.fg, 0))
|
|
border.width: modelData.page === root.selectedPage ? 1 : 0
|
|
border.color: Theme.alpha(Theme.accent, 0.26)
|
|
|
|
Rectangle {
|
|
width: 2
|
|
height: 18
|
|
radius: 1
|
|
anchors.left: parent.left
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
visible: navItem.modelData.page === root.selectedPage
|
|
gradient: Gradient {
|
|
GradientStop { position: 0; color: Theme.accent }
|
|
GradientStop { position: 1; color: Theme.accentSecondary }
|
|
}
|
|
}
|
|
|
|
Text {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 13
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 25
|
|
text: navItem.modelData.icon
|
|
color: navItem.modelData.page === root.selectedPage ? Theme.accent : Theme.fgDim
|
|
font.family: Theme.fontMono
|
|
font.pixelSize: 15
|
|
}
|
|
|
|
Text {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 47
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 9
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: navItem.modelData.label
|
|
color: navItem.modelData.page === root.selectedPage ? Theme.fg : Theme.fgDim
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSize
|
|
font.weight: navItem.modelData.page === root.selectedPage ? Font.Medium : Font.Normal
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
MouseArea {
|
|
id: navMouse
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: root.pageRequested(navItem.modelData.page)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: healthFooter
|
|
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.bottom: parent.bottom
|
|
height: 54
|
|
activeFocusOnTab: true
|
|
color: footerTap.hovered || activeFocus
|
|
? Theme.alpha(Theme.fg, 0.07)
|
|
: Theme.alpha(Theme.bg, 0.35)
|
|
border.width: activeFocus ? 2 : 0
|
|
border.color: Theme.accent
|
|
|
|
function footerText(): string {
|
|
if (Health.checks.length === 0)
|
|
return Health.diagnosticUnavailable ? "Health check unavailable" : "Checking the desktop";
|
|
if (Health.status === "error")
|
|
return "Desktop needs attention";
|
|
if (Health.status === "warning") {
|
|
const count = Health.summary.warnings + Health.summary.errors;
|
|
return count + (count === 1 ? " health observation" : " health observations");
|
|
}
|
|
return "Desktop is healthy";
|
|
}
|
|
|
|
function footerColor(): color {
|
|
if (Health.checks.length === 0)
|
|
return Health.diagnosticUnavailable ? Theme.danger : Theme.fgMuted;
|
|
if (Health.status === "error")
|
|
return Theme.danger;
|
|
if (Health.status === "warning")
|
|
return Theme.warn;
|
|
return Theme.ok;
|
|
}
|
|
|
|
Rectangle {
|
|
width: 7
|
|
height: 7
|
|
radius: 4
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 19
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
color: healthFooter.footerColor()
|
|
}
|
|
|
|
Text {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 36
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 12
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: healthFooter.footerText()
|
|
color: Theme.fgDim
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
TapHandler {
|
|
id: footerTap
|
|
onTapped: root.pageRequested("services")
|
|
}
|
|
|
|
Keys.onReturnPressed: root.pageRequested("services")
|
|
Keys.onSpacePressed: root.pageRequested("services")
|
|
}
|
|
}
|