Build the Panama Control Center

This commit is contained in:
Gabriel Brown
2026-08-17 13:44:18 -04:00
parent 190e3200e0
commit 5940d6b4bd
12 changed files with 867 additions and 8 deletions
+3
View File
@@ -8,3 +8,6 @@
/config/dot/nvim/lazy-lock.json
# Local visual-companion sessions and generated design mocks.
/.superpowers/
# Python helper bytecode is local runtime state.
__pycache__/
*.py[cod]
+2
View File
@@ -78,6 +78,8 @@ Singleton {
readonly property int popoverRadius: 18
readonly property int popoverPadding: 14
readonly property int popoverWidth: 380
readonly property int controlCenterWidth: 430
readonly property int controlCenterTopGap: 2
readonly property int cardRadius: 12
readonly property int pillRadius: 999
@@ -10,6 +10,7 @@ import Quickshell.Networking
import Quickshell.Services.Pipewire
import QtQuick
import qs.config
import qs.services
import qs.widgets
Pill {
@@ -106,4 +107,10 @@ Pill {
glyph: "\u{F0176}" // md-coffee
color: Theme.warn
}
StatusGlyph {
visible: KdeConnect.phoneReachable
glyph: "\u{F03F2}" // md-cellphone-link
color: Theme.cyan
}
}
@@ -0,0 +1,50 @@
import QtQuick
import qs.config
Item {
id: root
property string label: ""
property string action: ""
property bool actionEnabled: true
signal actionTriggered
implicitHeight: 24
Text {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: root.label.toUpperCase()
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
font.letterSpacing: 0.9
}
Text {
id: actionLabel
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: root.action !== ""
text: root.action
color: root.actionEnabled
? (actionMouse.containsMouse ? Theme.accentAlt : Theme.accent)
: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
Behavior on color { ColorAnimation { duration: Theme.durFast } }
MouseArea {
id: actionMouse
anchors.fill: parent
anchors.margins: -6
enabled: root.actionEnabled
hoverEnabled: true
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: root.actionTriggered()
}
}
}
@@ -0,0 +1,280 @@
import QtQuick
import qs.config
import qs.services
Item {
id: root
property bool expanded: false
signal toggleExpanded
implicitHeight: content.implicitHeight
Column {
id: content
width: parent.width
spacing: 6
ControlSectionHeader {
width: parent.width
label: "Home"
action: HomeAssistant.entities.length > 0
? HomeAssistant.entities.length + " accessories " + (root.expanded ? "⌃" : "")
: "Retry"
actionEnabled: HomeAssistant.phase !== "loading"
onActionTriggered: {
if (HomeAssistant.entities.length > 0)
root.toggleExpanded();
else
HomeAssistant.refresh();
}
}
Rectangle {
id: homeCard
width: parent.width
height: visible ? homeGrid.implicitHeight + 20 : 0
visible: HomeAssistant.entities.length > 0
radius: Theme.cardRadius + 2
color: Theme.alpha(Theme.accent, 0.045)
border.width: 1
border.color: Theme.alpha(Theme.accent, 0.12)
Grid {
id: homeGrid
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 10
columns: 4
spacing: 7
Repeater {
model: HomeAssistant.visibleEntities
HomeTile {
required property var modelData
width: (homeGrid.width - homeGrid.spacing * 3) / 4
entity: modelData
busy: HomeAssistant.busyEntityId === modelData.id
onActivated: HomeAssistant.toggleEntity(modelData.id)
}
}
}
}
Rectangle {
width: parent.width
height: visible ? 66 : 0
visible: HomeAssistant.entities.length === 0
radius: Theme.cardRadius
color: Theme.alpha(Theme.fg, 0.05)
border.width: 1
border.color: Theme.alpha(Theme.fg, 0.045)
Text {
id: stateIcon
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: HomeAssistant.phase === "loading" ? "\u{F0772}" : "\u{F02DC}"
color: HomeAssistant.phase === "loading" ? Theme.accent : Theme.fgDim
font.family: Theme.fontMono
font.pixelSize: 16
}
Column {
anchors.left: stateIcon.right
anchors.leftMargin: 11
anchors.right: openHome.left
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
spacing: 3
Text {
width: parent.width
text: root.emptyTitle()
color: Theme.fg
elide: Text.ElideRight
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
font.weight: Font.DemiBold
}
Text {
width: parent.width
text: root.emptyDetail()
color: Theme.fgDim
elide: Text.ElideRight
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}
Rectangle {
id: openHome
anchors.right: parent.right
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
width: 48
height: 28
radius: 9
visible: HomeAssistant.phase !== "loading"
color: openMouse.containsMouse
? Theme.alpha(Theme.accent, 0.20)
: Theme.alpha(Theme.accent, 0.11)
Text {
anchors.centerIn: parent
text: "Open"
color: Theme.accent
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
}
MouseArea {
id: openMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: HomeAssistant.open()
}
}
}
Rectangle {
width: parent.width
height: visible ? 30 : 0
visible: HomeAssistant.stale
radius: 9
color: Theme.alpha(Theme.warn, 0.08)
Text {
anchors.left: parent.left
anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter
text: "Showing the last known state"
color: Theme.warn
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
Text {
anchors.right: parent.right
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
text: "Retry"
color: Theme.accent
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
MouseArea {
anchors.fill: parent
anchors.margins: -6
cursorShape: Qt.PointingHandCursor
onClicked: HomeAssistant.refresh()
}
}
}
Section {
width: parent.width
expanded: root.expanded && HomeAssistant.entities.length > 0
ScrollColumn {
width: parent.width
maxHeight: 270
spacing: 2
Repeater {
model: HomeAssistant.entities
Rectangle {
id: entityRow
required property var modelData
width: parent.width
height: 44
radius: 10
color: entityMouse.containsMouse
? Theme.alpha(Theme.fg, 0.09)
: "transparent"
Text {
id: entityGlyph
anchors.left: parent.left
anchors.leftMargin: 11
anchors.verticalCenter: parent.verticalCenter
text: "\u{F0335}"
color: entityRow.modelData.active ? Theme.warn : Theme.fgDim
font.family: Theme.fontMono
font.pixelSize: 14
}
Column {
anchors.left: entityGlyph.right
anchors.leftMargin: 10
anchors.right: entityState.left
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Text {
width: parent.width
text: entityRow.modelData.name
color: Theme.fg
elide: Text.ElideRight
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
}
Text {
width: parent.width
text: !entityRow.modelData.available ? "Unavailable" : entityRow.modelData.state
color: Theme.fgMuted
elide: Text.ElideRight
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}
Text {
id: entityState
anchors.right: parent.right
anchors.rightMargin: 11
anchors.verticalCenter: parent.verticalCenter
text: HomeAssistant.busyEntityId === entityRow.modelData.id
? "Working"
: (entityRow.modelData.active ? "On" : "Off")
color: entityRow.modelData.active ? Theme.warn : Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
}
MouseArea {
id: entityMouse
anchors.fill: parent
enabled: entityRow.modelData.available && HomeAssistant.busyEntityId === ""
hoverEnabled: true
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: HomeAssistant.toggleEntity(entityRow.modelData.id)
}
}
}
}
}
}
function emptyTitle(): string {
if (HomeAssistant.phase === "loading")
return "Loading your home";
if (HomeAssistant.lastError === "authentication-required")
return "Authentication required";
if (HomeAssistant.lastError === "not-configured")
return "Home Assistant is not configured";
return "Home Assistant is unavailable";
}
function emptyDetail(): string {
if (HomeAssistant.phase === "loading")
return "Reading configured favourites";
if (HomeAssistant.lastError === "authentication-required")
return "Update the long-lived access token";
if (HomeAssistant.lastError === "not-configured")
return "Add a URL, token and favourites";
return "Check the connection and retry";
}
}
@@ -0,0 +1,81 @@
import QtQuick
import qs.config
Rectangle {
id: root
required property var entity
property bool busy: false
signal activated
implicitHeight: 72
radius: Theme.cardRadius
color: {
if (root.entity.active)
return Theme.alpha(Theme.warn, tileMouse.containsMouse ? 0.20 : 0.14);
return Theme.alpha(Theme.fg, tileMouse.containsMouse ? 0.10 : 0.055);
}
border.width: 1
border.color: root.entity.active
? Theme.alpha(Theme.warn, 0.20)
: Theme.alpha(Theme.fg, 0.045)
Behavior on color { ColorAnimation { duration: Theme.durFast } }
Text {
anchors.left: parent.left
anchors.leftMargin: 10
anchors.top: parent.top
anchors.topMargin: 9
text: root.busy ? "\u{F0772}" : root.glyphFor(root.entity.domain)
color: root.entity.active ? Theme.warn : Theme.fgDim
font.family: Theme.fontMono
font.pixelSize: 15
}
Column {
anchors.left: parent.left
anchors.leftMargin: 10
anchors.right: parent.right
anchors.rightMargin: 8
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
spacing: 1
Text {
width: parent.width
text: root.entity.name
color: Theme.fg
elide: Text.ElideRight
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
}
Text {
width: parent.width
text: !root.entity.available ? "Unavailable" : (root.entity.active ? "On" : "Off")
color: root.entity.active ? Theme.warn : Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: 9
}
}
MouseArea {
id: tileMouse
anchors.fill: parent
enabled: root.entity.available && !root.busy
hoverEnabled: true
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: root.activated()
}
function glyphFor(domain: string): string {
if (domain === "light")
return "\u{F0335}";
if (domain === "switch")
return "\u{F0521}";
if (domain === "scene")
return "\u{F0FCE}";
return "\u{F02DC}";
}
}
@@ -0,0 +1,273 @@
import QtQuick
import QtQuick.Dialogs
import qs.config
import qs.services
Item {
id: root
property bool expanded: false
signal toggleExpanded
readonly property var phone: KdeConnect.preferredPhone
readonly property var actionModels: [
{ id: "share", glyph: "\u{F0142}", label: "Send file" },
{ id: "clipboard", glyph: "\u{F014C}", label: "Clipboard" },
{ id: "ring", glyph: "\u{F009A}", label: "Ring" }
].filter(item => KdeConnect.supports(item.id))
implicitHeight: content.implicitHeight
Column {
id: content
width: parent.width
spacing: 6
ControlSectionHeader {
width: parent.width
label: "Phone"
action: root.phone ? (root.expanded ? "Details ⌃" : "Details ") : "Refresh"
onActionTriggered: {
if (root.phone)
root.toggleExpanded();
else
KdeConnect.refresh();
}
}
Rectangle {
width: parent.width
height: phoneContent.implicitHeight + 22
radius: Theme.cardRadius + 2
color: Theme.alpha(Theme.fg, 0.055)
border.width: 1
border.color: Theme.alpha(Theme.fg, 0.05)
Column {
id: phoneContent
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 11
spacing: 9
Item {
width: parent.width
height: 46
Rectangle {
id: phoneSilhouette
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 29
height: 43
radius: 8
color: Theme.alpha(Theme.accent, 0.07)
border.width: 1
border.color: Theme.alpha(Theme.fg, 0.42)
Rectangle {
anchors.top: parent.top
anchors.topMargin: 4
anchors.horizontalCenter: parent.horizontalCenter
width: 10
height: 2
radius: 1
color: Theme.alpha(Theme.fg, 0.35)
}
Rectangle {
anchors.right: parent.right
anchors.rightMargin: -3
anchors.top: parent.top
anchors.topMargin: 10
width: 6
height: 6
radius: 3
visible: KdeConnect.phoneReachable
color: Theme.ok
}
}
Column {
anchors.left: phoneSilhouette.right
anchors.leftMargin: 11
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 3
Text {
width: parent.width
text: root.phone?.name ?? (KdeConnect.available ? "No paired phone" : "KDE Connect unavailable")
color: Theme.fg
elide: Text.ElideRight
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
font.weight: Font.DemiBold
}
Text {
width: parent.width
text: root.phone
? (KdeConnect.phoneReachable ? "● Nearby on Wi-Fi" : "Not nearby")
: (KdeConnect.available ? "Pair a device to continue" : "Open settings to repair the service")
color: KdeConnect.phoneReachable ? Theme.ok : Theme.fgDim
elide: Text.ElideRight
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}
}
Grid {
id: actionsGrid
width: parent.width
columns: Math.max(1, root.actionModels.length)
spacing: 7
visible: root.actionModels.length > 0
Repeater {
model: root.actionModels
Rectangle {
id: actionButton
required property var modelData
width: (actionsGrid.width - actionsGrid.spacing * Math.max(0, root.actionModels.length - 1)) / Math.max(1, root.actionModels.length)
height: 48
radius: 11
color: actionMouse.containsMouse && actionMouse.enabled
? Theme.alpha(Theme.accent, 0.15)
: Theme.alpha(Theme.accent, 0.075)
opacity: actionMouse.enabled ? 1 : 0.48
Text {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 7
text: actionButton.modelData.glyph
color: Theme.accent
font.family: Theme.fontMono
font.pixelSize: 13
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: 6
text: actionButton.modelData.label
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: 9
font.weight: Font.Medium
}
MouseArea {
id: actionMouse
anchors.fill: parent
enabled: KdeConnect.phoneReachable && !KdeConnect.transferActive
hoverEnabled: true
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: root.invoke(actionButton.modelData.id)
}
}
}
}
Text {
width: parent.width
visible: root.phone && !KdeConnect.phoneReachable && root.actionModels.length > 0
text: "Actions become available when the iPhone reconnects"
color: Theme.fgMuted
horizontalAlignment: Text.AlignHCenter
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
Text {
width: parent.width
visible: KdeConnect.lastError !== ""
text: root.errorText(KdeConnect.lastError)
color: Theme.warn
horizontalAlignment: Text.AlignHCenter
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
Rectangle {
width: parent.width
height: 1
visible: KdeConnect.recentExchange !== null
color: Theme.alpha(Theme.fg, 0.07)
}
RecentExchange {
width: parent.width
height: visible ? implicitHeight : 0
visible: KdeConnect.recentExchange !== null
exchange: KdeConnect.recentExchange ?? ({})
}
}
}
Section {
width: parent.width
expanded: root.expanded && root.phone !== null
Column {
width: parent.width
spacing: 2
RowButton {
width: parent.width
icon: "network-wireless-symbolic"
label: "Connection"
sublabel: KdeConnect.phoneReachable ? "Nearby on Wi-Fi" : "Waiting for the phone"
selected: KdeConnect.phoneReachable
onClicked: KdeConnect.refresh()
}
RowButton {
width: parent.width
icon: "preferences-system-symbolic"
label: "Device settings"
sublabel: "Pairing, plugins and service health"
onClicked: {
ShellState.close();
ShellState.openSettings("connectivity");
}
}
}
}
}
FileDialog {
id: fileDialog
title: root.phone ? "Send a file to " + root.phone.name : "Send a file"
fileMode: FileDialog.OpenFile
nameFilters: ["All files (*)"]
onAccepted: {
const path = root.localPath(selectedFile);
if (path !== "")
KdeConnect.sendFile(path);
}
}
function invoke(action: string): void {
if (action === "share")
fileDialog.open();
else if (action === "clipboard")
KdeConnect.sendClipboard();
else if (action === "ring")
KdeConnect.ring();
}
function localPath(selectedUrl: url): string {
const value = String(selectedUrl);
if (!value.startsWith("file://"))
return "";
return decodeURIComponent(value.slice(7));
}
function errorText(code: string): string {
if (code === "device-offline")
return "The iPhone is not nearby";
if (code === "unsupported-action")
return "That action is not available on this iPhone";
return "The phone action did not finish";
}
}
@@ -18,15 +18,21 @@ PanelWindow {
visible: ShellState.quickSettingsOpen
color: "transparent"
// Hangs off the top-right corner, clear of the floating bar.
// Hangs directly beneath the top-right status cluster. Two logical pixels
// preserve the Prism edge without making the surface feel detached.
anchors.top: true
anchors.right: true
margins.top: Theme.barHeight + Theme.barGap * 2
margins.top: Theme.barHeight + Theme.controlCenterTopGap
margins.right: Theme.barSideMargin
exclusiveZone: 0
implicitWidth: Theme.popoverWidth
implicitWidth: Theme.controlCenterWidth
implicitHeight: panel.implicitHeight
readonly property string expandedSection: panel.expandedSection
function expand(name: string): void {
panel.expand(name);
}
// The `qs-popover` prefix is matched by a layerrule in hypr/rules.lua.
WlrLayershell.namespace: "qs-popover-quicksettings"
@@ -36,11 +42,14 @@ PanelWindow {
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
onVisibleChanged: {
if (root.visible)
if (root.visible) {
KdeConnect.refresh();
HomeAssistant.refresh();
openAnim.restart();
else
} else {
panel.reset();
}
}
// Closes the panel when a click lands anywhere else on the desktop.
HyprlandFocusGrab {
@@ -18,7 +18,7 @@ import qs.widgets
Item {
id: root
implicitWidth: Theme.popoverWidth
implicitWidth: Theme.controlCenterWidth
implicitHeight: content.implicitHeight + Theme.popoverPadding * 2
// "" | "wifi" | "bluetooth" | "sink" | "source". Only one detail list is
@@ -233,6 +233,19 @@ Item {
width: content.width
}
// ── Connected life ─────────────────────────────────────────────────
HomeControls {
width: content.width
expanded: root.expandedSection === "home"
onToggleExpanded: root.expand("home")
}
PhoneControls {
width: content.width
expanded: root.expandedSection === "phone"
onToggleExpanded: root.expand("phone")
}
Rectangle {
width: content.width
height: 1
@@ -0,0 +1,64 @@
import QtQuick
import qs.config
Item {
id: root
required property var exchange
implicitHeight: 40
Rectangle {
width: 24
height: 24
radius: 8
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
color: Theme.alpha(Theme.ok, 0.10)
Text {
anchors.centerIn: parent
text: "\u{F012C}"
color: Theme.ok
font.family: Theme.fontMono
font.pixelSize: 12
}
}
Column {
anchors.left: parent.left
anchors.leftMargin: 34
anchors.right: timeLabel.left
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Text {
width: parent.width
text: root.exchange.fileName || "File"
color: Theme.fg
elide: Text.ElideMiddle
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
}
Text {
width: parent.width
text: "Sent to " + (root.exchange.deviceName || "phone")
color: Theme.fgMuted
elide: Text.ElideRight
font.family: Theme.fontFamily
font.pixelSize: 9
}
}
Text {
id: timeLabel
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: "Now"
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: 9
}
}
+6 -2
View File
@@ -78,7 +78,7 @@ ShellRoot {
// overview or quick settings is instant rather than paying a load.
Overview {}
// QuickSettings is the window; QuickSettingsPanel is its content.
QuickSettings {}
QuickSettings { id: quickSettings }
DateMenu { id: dateMenu }
ClipboardPanel {}
CaptureOverlay {}
@@ -223,8 +223,12 @@ ShellRoot {
function toggle(): void { ShellState.toggle("quicksettings"); }
function open(): void { ShellState.open("quicksettings"); }
function close(): void { ShellState.close(); }
function section(name: string): void { quickSettings.expand(name); }
function status(): string {
return JSON.stringify({ open: ShellState.quickSettingsOpen });
return JSON.stringify({
open: ShellState.quickSettingsOpen,
expandedSection: quickSettings.expandedSection
});
}
}
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
set -euo pipefail
fail() {
printf 'Control Center contract: %s\n' "$1" >&2
exit 1
}
project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cleanup() {
qs ipc call quicksettings close >/dev/null 2>&1 || true
qs ipc call kdeconnect reset >/dev/null 2>&1 || true
qs ipc call home-assistant reset >/dev/null 2>&1 || true
}
trap cleanup EXIT
rg -Fq 'readonly property int controlCenterWidth: 430' \
"$project_root/config/dot/quickshell/config/Theme.qml" \
|| fail 'approved Control Center width is missing'
rg -Fq 'readonly property int controlCenterTopGap: 2' \
"$project_root/config/dot/quickshell/config/Theme.qml" \
|| fail 'approved top attachment is missing'
rg -Fq 'margins.top: Theme.barHeight + Theme.controlCenterTopGap' \
"$project_root/config/dot/quickshell/modules/quicksettings/QuickSettings.qml" \
|| fail 'Control Center is not tightly attached to the bar'
rg -Fq 'implicitWidth: Theme.controlCenterWidth' \
"$project_root/config/dot/quickshell/modules/quicksettings/QuickSettings.qml" \
|| fail 'Control Center window does not use its geometry token'
rg -Fq 'HomeControls' \
"$project_root/config/dot/quickshell/modules/quicksettings/QuickSettingsPanel.qml" \
|| fail 'Home controls are not mounted'
rg -Fq 'PhoneControls' \
"$project_root/config/dot/quickshell/modules/quicksettings/QuickSettingsPanel.qml" \
|| fail 'Phone controls are not mounted'
rg -Fq 'visible: KdeConnect.phoneReachable' \
"$project_root/config/dot/quickshell/modules/bar/StatusCluster.qml" \
|| fail 'reachable phone state is absent from the bar'
for component in ControlSectionHeader HomeControls HomeTile PhoneControls RecentExchange; do
[[ -f "$project_root/config/dot/quickshell/modules/quicksettings/$component.qml" ]] \
|| fail "$component is missing"
done
qs ipc call home-assistant fixture ready >/dev/null
qs ipc call kdeconnect fixture reachable >/dev/null
qs ipc call quicksettings open >/dev/null
hyprctl layers | rg -q 'namespace: qs-popover-quicksettings' \
|| fail 'Control Center layer did not map'
jq -e '.open == true and .expandedSection == ""' \
<<<"$(qs ipc call quicksettings status)" >/dev/null \
|| fail 'Control Center did not open in its resting state'
qs ipc call quicksettings section home >/dev/null
jq -e '.open == true and .expandedSection == "home"' \
<<<"$(qs ipc call quicksettings status)" >/dev/null \
|| fail 'Home section did not expand'
qs ipc call quicksettings section phone >/dev/null
jq -e '.open == true and .expandedSection == "phone"' \
<<<"$(qs ipc call quicksettings status)" >/dev/null \
|| fail 'Phone did not replace Home as the expanded section'
qs ipc call quicksettings section phone >/dev/null
jq -e '.open == true and .expandedSection == ""' \
<<<"$(qs ipc call quicksettings status)" >/dev/null \
|| fail 'expanded Phone section did not collapse'
trap - EXIT
cleanup
printf 'Control Center contract: PASS\n'