Add Home and Phone settings
This commit is contained in:
@@ -0,0 +1,75 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var entity
|
||||||
|
signal addRequested(string id)
|
||||||
|
|
||||||
|
width: parent ? parent.width : 620
|
||||||
|
implicitHeight: 62
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: stateCopy.left
|
||||||
|
anchors.rightMargin: 16
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 3
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: root.entity.sourceName
|
||||||
|
color: Theme.fg
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: root.entity.id
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: stateCopy
|
||||||
|
anchors.right: addButton.left
|
||||||
|
anchors.rightMargin: 14
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 92
|
||||||
|
text: root.entity.available === false ? "Unavailable" : String(root.entity.state || "Unknown")
|
||||||
|
color: root.entity.available === false ? Theme.fgMuted : Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
id: addButton
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "Add"
|
||||||
|
tone: "accent"
|
||||||
|
activeFocusOnTab: true
|
||||||
|
border.width: activeFocus ? 2 : 0
|
||||||
|
border.color: activeFocus ? Theme.fg : Theme.alpha(Theme.fg, 0)
|
||||||
|
onClicked: root.addRequested(root.entity.id)
|
||||||
|
Keys.onReturnPressed: root.addRequested(root.entity.id)
|
||||||
|
Keys.onSpacePressed: root.addRequested(root.entity.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
height: 1
|
||||||
|
color: Theme.alpha(Theme.fg, 0.06)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
import qs.widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var favorite
|
||||||
|
required property string sourceName
|
||||||
|
required property int index
|
||||||
|
required property bool featured
|
||||||
|
|
||||||
|
signal aliasCommitted(string id, string alias)
|
||||||
|
signal removeRequested(string id)
|
||||||
|
signal moveRequested(string id, int targetIndex)
|
||||||
|
|
||||||
|
readonly property bool dragging: dragHandler.active
|
||||||
|
|
||||||
|
implicitHeight: 108
|
||||||
|
radius: Theme.cardRadius
|
||||||
|
color: root.dragging
|
||||||
|
? Theme.mix(Theme.bgDark, Theme.accent, 0.09)
|
||||||
|
: Theme.alpha(Theme.bgDark, 0.7)
|
||||||
|
border.width: root.dragging ? 2 : 1
|
||||||
|
border.color: root.dragging
|
||||||
|
? Theme.alpha(Theme.accent, 0.82)
|
||||||
|
: Theme.alpha(Theme.fg, 0.07)
|
||||||
|
z: root.dragging ? 10 : 0
|
||||||
|
|
||||||
|
transform: Translate {
|
||||||
|
x: root.dragging ? dragHandler.translation.x : 0
|
||||||
|
y: root.dragging ? dragHandler.translation.y : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
PrismEdge {
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
inset: root.radius
|
||||||
|
opacity: root.dragging ? 0.82 : 0.2
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: dragHandle
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 11
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 30
|
||||||
|
height: 42
|
||||||
|
radius: 9
|
||||||
|
activeFocusOnTab: true
|
||||||
|
color: root.dragging || activeFocus
|
||||||
|
? Theme.alpha(Theme.accent, 0.14)
|
||||||
|
: (handleMouse.containsMouse ? Theme.alpha(Theme.fg, 0.09) : Theme.alpha(Theme.fg, 0.045))
|
||||||
|
border.width: activeFocus ? 2 : 1
|
||||||
|
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.06)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "⠿"
|
||||||
|
color: root.dragging ? Theme.accent : Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: handleMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
cursorShape: Qt.SizeAllCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
DragHandler {
|
||||||
|
id: dragHandler
|
||||||
|
target: null
|
||||||
|
onActiveChanged: {
|
||||||
|
if (!active)
|
||||||
|
root.commitDrag();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if (event.key === Qt.Key_Left || event.key === Qt.Key_Up) {
|
||||||
|
root.moveRequested(root.favorite.id, Math.max(0, root.index - 1));
|
||||||
|
event.accepted = true;
|
||||||
|
} else if (event.key === Qt.Key_Right || event.key === Qt.Key_Down) {
|
||||||
|
const grid = root.GridView.view;
|
||||||
|
const lastIndex = grid ? grid.count - 1 : root.index;
|
||||||
|
root.moveRequested(root.favorite.id, Math.min(lastIndex, root.index + 1));
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: aliasFrame
|
||||||
|
anchors.left: dragHandle.right
|
||||||
|
anchors.leftMargin: 10
|
||||||
|
anchors.right: removeButton.left
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: 12
|
||||||
|
height: 34
|
||||||
|
radius: 8
|
||||||
|
color: Theme.alpha(Theme.fg, aliasInput.activeFocus ? 0.075 : 0.045)
|
||||||
|
border.width: aliasInput.activeFocus ? 2 : 1
|
||||||
|
border.color: aliasInput.activeFocus
|
||||||
|
? Theme.alpha(Theme.accent, 0.78)
|
||||||
|
: Theme.alpha(Theme.fg, 0.065)
|
||||||
|
|
||||||
|
TextInput {
|
||||||
|
id: aliasInput
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 10
|
||||||
|
anchors.rightMargin: 10
|
||||||
|
activeFocusOnTab: true
|
||||||
|
text: String(root.favorite.alias || "")
|
||||||
|
color: Theme.fg
|
||||||
|
selectionColor: Theme.accent
|
||||||
|
selectedTextColor: Theme.bgDark
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
font.weight: Font.Medium
|
||||||
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
|
selectByMouse: true
|
||||||
|
clip: true
|
||||||
|
onEditingFinished: root.aliasCommitted(root.favorite.id, text)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: aliasInput.text === "" && !aliasInput.activeFocus
|
||||||
|
text: root.sourceName
|
||||||
|
color: Theme.fgDim
|
||||||
|
font: aliasInput.font
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: aliasFrame.left
|
||||||
|
anchors.right: removeButton.left
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
anchors.top: aliasFrame.bottom
|
||||||
|
anchors.topMargin: 7
|
||||||
|
text: root.sourceName
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: aliasFrame.left
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: 10
|
||||||
|
width: badgeCopy.implicitWidth + 14
|
||||||
|
height: 21
|
||||||
|
radius: Theme.pillRadius
|
||||||
|
visible: root.featured
|
||||||
|
color: Theme.alpha(Theme.accent, 0.1)
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.alpha(Theme.accent, 0.2)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: badgeCopy
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Control Center"
|
||||||
|
color: Theme.accent
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: 9
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
id: removeButton
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 11
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "Remove"
|
||||||
|
activeFocusOnTab: true
|
||||||
|
border.width: activeFocus ? 2 : 1
|
||||||
|
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
||||||
|
onClicked: root.removeRequested(root.favorite.id)
|
||||||
|
Keys.onReturnPressed: root.removeRequested(root.favorite.id)
|
||||||
|
Keys.onSpacePressed: root.removeRequested(root.favorite.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitDrag(): void {
|
||||||
|
const grid = root.GridView.view;
|
||||||
|
if (!grid || grid.count <= 0)
|
||||||
|
return;
|
||||||
|
const centerX = root.x + dragHandler.translation.x + root.width / 2;
|
||||||
|
const centerY = root.y + dragHandler.translation.y + root.height / 2;
|
||||||
|
const modelCount = grid.count;
|
||||||
|
const column = Math.max(0, Math.min(1, Math.floor(centerX / grid.cellWidth)));
|
||||||
|
const row = Math.max(0, Math.floor(centerY / grid.cellHeight));
|
||||||
|
root.moveRequested(root.favorite.id, Math.min(modelCount - 1, row * 2 + column));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string lightQuery: searchInput.text.trim().toLowerCase()
|
||||||
|
|
||||||
|
readonly property var availableLights: HomeAssistant.catalog.filter(entity => {
|
||||||
|
if (HomePreferences.isSelected(entity.id))
|
||||||
|
return false;
|
||||||
|
const haystack = (entity.sourceName + " " + entity.id).toLowerCase();
|
||||||
|
return root.lightQuery === "" || haystack.includes(root.lightQuery);
|
||||||
|
})
|
||||||
|
|
||||||
|
function homeStatus(): string {
|
||||||
|
if (HomeAssistant.phase === "ready")
|
||||||
|
return `Connected · ${HomeAssistant.discoveredCount} lights discovered`;
|
||||||
|
if (HomeAssistant.phase === "degraded")
|
||||||
|
return "Last update unavailable · showing saved controls";
|
||||||
|
if (HomeAssistant.lastError === "authentication-required")
|
||||||
|
return "Authentication required";
|
||||||
|
if (HomeAssistant.lastError === "not-configured")
|
||||||
|
return "Home Assistant is not configured";
|
||||||
|
if (HomeAssistant.phase === "loading")
|
||||||
|
return "Connecting to Home Assistant";
|
||||||
|
return "Home Assistant is unavailable";
|
||||||
|
}
|
||||||
|
|
||||||
|
Flickable {
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
contentWidth: width
|
||||||
|
contentHeight: content.implicitHeight + 64
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: content
|
||||||
|
width: parent.width - 68
|
||||||
|
x: 34
|
||||||
|
y: 30
|
||||||
|
spacing: 16
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Home & Phone"
|
||||||
|
color: Theme.fg
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: 27
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Choose what appears in Control Center and keep phone continuity close at hand."
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
bottomPadding: 6
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Home Assistant"
|
||||||
|
subtitle: root.homeStatus()
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
label: "Light catalog"
|
||||||
|
detail: "Panama reads light state through the Home Assistant helper."
|
||||||
|
divider: false
|
||||||
|
controlWidth: 176
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
id: refreshButton
|
||||||
|
text: "Refresh"
|
||||||
|
activeFocusOnTab: true
|
||||||
|
border.width: activeFocus ? 2 : 1
|
||||||
|
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
||||||
|
onClicked: HomeAssistant.refresh()
|
||||||
|
Keys.onReturnPressed: HomeAssistant.refresh()
|
||||||
|
Keys.onSpacePressed: HomeAssistant.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
id: openHomeButton
|
||||||
|
text: "Open"
|
||||||
|
activeFocusOnTab: true
|
||||||
|
border.width: activeFocus ? 2 : 1
|
||||||
|
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
||||||
|
onClicked: HomeAssistant.open()
|
||||||
|
Keys.onReturnPressed: HomeAssistant.open()
|
||||||
|
Keys.onSpacePressed: HomeAssistant.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Control Center lights"
|
||||||
|
subtitle: HomeAssistant.selectedEntities.length === 0
|
||||||
|
? "Select the lights that belong on your shelf."
|
||||||
|
: `${Math.min(4, HomeAssistant.selectedEntities.length)} in Control Center · ${HomeAssistant.selectedEntities.length} selected`
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: HomeAssistant.selectedEntities.length === 0
|
||||||
|
text: "Choose lights below to build your Control Center shelf."
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
topPadding: 3
|
||||||
|
bottomPadding: 13
|
||||||
|
}
|
||||||
|
|
||||||
|
GridView {
|
||||||
|
id: favoritesGrid
|
||||||
|
width: parent.width
|
||||||
|
height: Math.ceil(count / 2) * cellHeight
|
||||||
|
visible: count > 0
|
||||||
|
interactive: false
|
||||||
|
clip: false
|
||||||
|
cellWidth: width / 2
|
||||||
|
cellHeight: 116
|
||||||
|
model: HomeAssistant.selectedEntities
|
||||||
|
|
||||||
|
delegate: HomeFavoriteCard {
|
||||||
|
required property var modelData
|
||||||
|
width: GridView.view.cellWidth - 6
|
||||||
|
height: 108
|
||||||
|
favorite: ({
|
||||||
|
id: modelData.id,
|
||||||
|
alias: modelData.name === modelData.sourceName ? "" : modelData.name
|
||||||
|
})
|
||||||
|
sourceName: modelData.sourceName
|
||||||
|
featured: index < 4
|
||||||
|
onAliasCommitted: (id, alias) => HomePreferences.setAlias(id, alias)
|
||||||
|
onMoveRequested: (id, targetIndex) => HomePreferences.move(id, targetIndex)
|
||||||
|
onRemoveRequested: id => HomePreferences.remove(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 46
|
||||||
|
visible: HomePreferences.saveError !== ""
|
||||||
|
radius: 9
|
||||||
|
color: Theme.alpha(Theme.warn, 0.09)
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.alpha(Theme.warn, 0.24)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.right: retryButton.left
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: HomePreferences.saveError
|
||||||
|
color: Theme.warn
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
id: retryButton
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 8
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "Retry"
|
||||||
|
activeFocusOnTab: true
|
||||||
|
border.width: activeFocus ? 2 : 1
|
||||||
|
border.color: activeFocus ? Theme.warn : Theme.alpha(Theme.fg, 0.08)
|
||||||
|
onClicked: HomePreferences.retrySave()
|
||||||
|
Keys.onReturnPressed: HomePreferences.retrySave()
|
||||||
|
Keys.onSpacePressed: HomePreferences.retrySave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Available lights"
|
||||||
|
subtitle: "Search the Home Assistant catalog by source name or entity ID."
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 38
|
||||||
|
radius: 10
|
||||||
|
color: Theme.alpha(Theme.fg, searchInput.activeFocus ? 0.08 : 0.05)
|
||||||
|
border.width: searchInput.activeFocus ? 2 : 1
|
||||||
|
border.color: searchInput.activeFocus
|
||||||
|
? Theme.alpha(Theme.accent, 0.72)
|
||||||
|
: Theme.alpha(Theme.fg, 0.065)
|
||||||
|
|
||||||
|
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: 36
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 11
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
activeFocusOnTab: true
|
||||||
|
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 available lights"
|
||||||
|
color: Theme.fgMuted
|
||||||
|
font: searchInput.font
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
visible: root.availableLights.length > 0
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.availableLights
|
||||||
|
|
||||||
|
AvailableLightRow {
|
||||||
|
required property var modelData
|
||||||
|
width: parent.width
|
||||||
|
entity: modelData
|
||||||
|
onAddRequested: id => HomePreferences.add(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: root.availableLights.length === 0
|
||||||
|
text: root.lightQuery === "" && HomeAssistant.catalog.length > 0
|
||||||
|
? "All discovered lights are already selected"
|
||||||
|
: "No lights match that search"
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
topPadding: 18
|
||||||
|
bottomPadding: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Phone continuity"
|
||||||
|
subtitle: "Keep the Messages handoff independent from phone connectivity."
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
label: "Messages"
|
||||||
|
detail: "Opens BlueBubbles"
|
||||||
|
divider: false
|
||||||
|
controlWidth: 204
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 12
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: SystemSettings.bluebubblesAvailable ? "Installed" : "Unavailable"
|
||||||
|
color: SystemSettings.bluebubblesAvailable ? Theme.ok : Theme.fgMuted
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
id: openBlueBubblesButton
|
||||||
|
text: "Open"
|
||||||
|
enabled: SystemSettings.bluebubblesAvailable
|
||||||
|
activeFocusOnTab: enabled
|
||||||
|
border.width: activeFocus ? 2 : 1
|
||||||
|
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
||||||
|
onClicked: SystemSettings.openApplication("bluebubbles")
|
||||||
|
Keys.onReturnPressed: if (enabled) SystemSettings.openApplication("bluebubbles")
|
||||||
|
Keys.onSpacePressed: if (enabled) SystemSettings.openApplication("bluebubbles")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,6 +88,7 @@ Rectangle {
|
|||||||
case "appearance": return appearancePage;
|
case "appearance": return appearancePage;
|
||||||
case "displays": return displaysPage;
|
case "displays": return displaysPage;
|
||||||
case "connectivity": return connectivityPage;
|
case "connectivity": return connectivityPage;
|
||||||
|
case "home-phone": return homePhonePage;
|
||||||
case "desktop": return desktopPage;
|
case "desktop": return desktopPage;
|
||||||
case "sound": return soundPage;
|
case "sound": return soundPage;
|
||||||
case "notifications": return notificationsPage;
|
case "notifications": return notificationsPage;
|
||||||
@@ -133,6 +134,7 @@ Rectangle {
|
|||||||
Component { id: appearancePage; AppearancePage {} }
|
Component { id: appearancePage; AppearancePage {} }
|
||||||
Component { id: displaysPage; DisplaysPage {} }
|
Component { id: displaysPage; DisplaysPage {} }
|
||||||
Component { id: connectivityPage; ConnectivityPage {} }
|
Component { id: connectivityPage; ConnectivityPage {} }
|
||||||
|
Component { id: homePhonePage; HomePhonePage {} }
|
||||||
Component { id: desktopPage; DesktopPage {} }
|
Component { id: desktopPage; DesktopPage {} }
|
||||||
Component { id: soundPage; SoundPage {} }
|
Component { id: soundPage; SoundPage {} }
|
||||||
Component { id: notificationsPage; NotificationsPage {} }
|
Component { id: notificationsPage; NotificationsPage {} }
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ Rectangle {
|
|||||||
{ page: "appearance", label: "Appearance", icon: "\u{F0E0D}" },
|
{ page: "appearance", label: "Appearance", icon: "\u{F0E0D}" },
|
||||||
{ page: "displays", label: "Displays", icon: "\u{F0379}" },
|
{ page: "displays", label: "Displays", icon: "\u{F0379}" },
|
||||||
{ page: "connectivity", label: "Network & Devices", icon: "\u{F08D4}" },
|
{ page: "connectivity", label: "Network & Devices", icon: "\u{F08D4}" },
|
||||||
|
{ page: "home-phone", label: "Home & Phone", icon: "\u{F02DC}" },
|
||||||
{ page: "desktop", label: "Desktop & Dock", icon: "\u{F04A4}" },
|
{ page: "desktop", label: "Desktop & Dock", icon: "\u{F04A4}" },
|
||||||
{ page: "sound", label: "Sound", icon: "\u{F057E}" },
|
{ page: "sound", label: "Sound", icon: "\u{F057E}" },
|
||||||
{ page: "notifications", label: "Notifications & Focus", icon: "\u{F009A}" },
|
{ page: "notifications", label: "Notifications & Focus", icon: "\u{F009A}" },
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ module qs.modules.settings
|
|||||||
AboutPage 1.0 AboutPage.qml
|
AboutPage 1.0 AboutPage.qml
|
||||||
AppearancePage 1.0 AppearancePage.qml
|
AppearancePage 1.0 AppearancePage.qml
|
||||||
ConnectivityPage 1.0 ConnectivityPage.qml
|
ConnectivityPage 1.0 ConnectivityPage.qml
|
||||||
|
HomePhonePage 1.0 HomePhonePage.qml
|
||||||
|
HomeFavoriteCard 1.0 HomeFavoriteCard.qml
|
||||||
|
AvailableLightRow 1.0 AvailableLightRow.qml
|
||||||
DesktopPage 1.0 DesktopPage.qml
|
DesktopPage 1.0 DesktopPage.qml
|
||||||
DisplaysPage 1.0 DisplaysPage.qml
|
DisplaysPage 1.0 DisplaysPage.qml
|
||||||
HomePage 1.0 HomePage.qml
|
HomePage 1.0 HomePage.qml
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openSettings(page: string): void {
|
function openSettings(page: string): void {
|
||||||
const allowed = ["home", "appearance", "displays", "connectivity", "desktop", "sound", "notifications", "screen-intelligence", "shortcuts", "services", "about"];
|
const allowed = ["home", "appearance", "displays", "connectivity", "home-phone", "desktop", "sound", "notifications", "screen-intelligence", "shortcuts", "services", "about"];
|
||||||
root.settingsPage = allowed.indexOf(page) >= 0 ? page : "home";
|
root.settingsPage = allowed.indexOf(page) >= 0 ? page : "home";
|
||||||
DesktopPreferences.lastPage = root.settingsPage;
|
DesktopPreferences.lastPage = root.settingsPage;
|
||||||
root.settingsOpen = true;
|
root.settingsOpen = true;
|
||||||
|
|||||||
@@ -27,13 +27,16 @@ Singleton {
|
|||||||
property bool hyprpaperActive: false
|
property bool hyprpaperActive: false
|
||||||
property bool hypridleActive: false
|
property bool hypridleActive: false
|
||||||
property bool vicinaeActive: false
|
property bool vicinaeActive: false
|
||||||
|
property bool bluebubblesDetected: false
|
||||||
|
|
||||||
property string hyprlandVersion: ""
|
property string hyprlandVersion: ""
|
||||||
property string quickshellVersion: "0.3.0"
|
property string quickshellVersion: "0.3.0"
|
||||||
property string lastError: ""
|
property string lastError: ""
|
||||||
|
|
||||||
readonly property bool busy: monitorQuery.running || serviceQuery.running || versionQuery.running
|
readonly property bool busy: monitorQuery.running || serviceQuery.running || versionQuery.running
|
||||||
|| autoHdrWrite.running || vrrWrite.running || directScanoutWrite.running
|
|| bluebubblesQuery.running || autoHdrWrite.running || vrrWrite.running || directScanoutWrite.running
|
||||||
|
|
||||||
|
readonly property bool bluebubblesAvailable: root.bluebubblesDetected
|
||||||
|
|
||||||
readonly property bool autoHdr: DesktopPreferences.autoHdr
|
readonly property bool autoHdr: DesktopPreferences.autoHdr
|
||||||
readonly property int vrrPolicy: DesktopPreferences.vrrPolicy
|
readonly property int vrrPolicy: DesktopPreferences.vrrPolicy
|
||||||
@@ -79,6 +82,12 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: bluebubblesQuery
|
||||||
|
command: ["flatpak", "info", "app.bluebubbles.BlueBubbles"]
|
||||||
|
onExited: (exitCode, exitStatus) => root.bluebubblesDetected = exitCode === 0
|
||||||
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: autoHdrWrite
|
id: autoHdrWrite
|
||||||
property bool requested: true
|
property bool requested: true
|
||||||
@@ -136,6 +145,8 @@ Singleton {
|
|||||||
serviceQuery.running = true;
|
serviceQuery.running = true;
|
||||||
if (!versionQuery.running && !root.hyprlandVersion)
|
if (!versionQuery.running && !root.hyprlandVersion)
|
||||||
versionQuery.running = true;
|
versionQuery.running = true;
|
||||||
|
if (!bluebubblesQuery.running)
|
||||||
|
bluebubblesQuery.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseMonitors(text: string): void {
|
function parseMonitors(text: string): void {
|
||||||
@@ -225,7 +236,8 @@ Singleton {
|
|||||||
"nextcloud": ["nextcloud"],
|
"nextcloud": ["nextcloud"],
|
||||||
"rustdesk": ["rustdesk"],
|
"rustdesk": ["rustdesk"],
|
||||||
"kdeconnect": ["kdeconnect-app"],
|
"kdeconnect": ["kdeconnect-app"],
|
||||||
"mission-center": ["flatpak", "run", "io.missioncenter.MissionCenter"]
|
"mission-center": ["flatpak", "run", "io.missioncenter.MissionCenter"],
|
||||||
|
"bluebubbles": ["flatpak", "run", "app.bluebubbles.BlueBubbles"]
|
||||||
};
|
};
|
||||||
const command = commands[id];
|
const command = commands[id];
|
||||||
if (!command) {
|
if (!command) {
|
||||||
|
|||||||
@@ -291,7 +291,12 @@ ShellRoot {
|
|||||||
function close(): void { ShellState.closeSettings(); }
|
function close(): void { ShellState.closeSettings(); }
|
||||||
function page(name: string): void { ShellState.openSettings(name); }
|
function page(name: string): void { ShellState.openSettings(name); }
|
||||||
function status(): string {
|
function status(): string {
|
||||||
return JSON.stringify({ open: ShellState.settingsOpen, page: ShellState.settingsPage });
|
return JSON.stringify({
|
||||||
|
open: ShellState.settingsOpen,
|
||||||
|
page: ShellState.settingsPage,
|
||||||
|
discoveredCount: HomeAssistant.discoveredCount,
|
||||||
|
selectedCount: HomeAssistant.configuredCount
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,6 +311,7 @@ ShellRoot {
|
|||||||
autoHdr: SystemSettings.autoHdr,
|
autoHdr: SystemSettings.autoHdr,
|
||||||
vrrPolicy: SystemSettings.vrrPolicy,
|
vrrPolicy: SystemSettings.vrrPolicy,
|
||||||
directScanoutPolicy: SystemSettings.directScanoutPolicy,
|
directScanoutPolicy: SystemSettings.directScanoutPolicy,
|
||||||
|
bluebubblesAvailable: SystemSettings.bluebubblesAvailable,
|
||||||
busy: SystemSettings.busy,
|
busy: SystemSettings.busy,
|
||||||
lastError: SystemSettings.lastError
|
lastError: SystemSettings.lastError
|
||||||
});
|
});
|
||||||
|
|||||||
+215
@@ -0,0 +1,215 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
state_home="$(mktemp -d /tmp/panama-home-phone-settings-state.XXXXXX)"
|
||||||
|
source_config_path="$repo_dir/config/dot/quickshell"
|
||||||
|
config_path="$state_home/quickshell"
|
||||||
|
test_bin="$state_home/bin"
|
||||||
|
shell_log="$state_home/quickshell.log"
|
||||||
|
flatpak_log="$state_home/flatpak.log"
|
||||||
|
|
||||||
|
cleanup_bootstrap() {
|
||||||
|
rm -rf "$state_home"
|
||||||
|
}
|
||||||
|
trap cleanup_bootstrap EXIT
|
||||||
|
|
||||||
|
mkdir -p "$test_bin"
|
||||||
|
cp -a "$source_config_path" "$config_path"
|
||||||
|
: >"$flatpak_log"
|
||||||
|
|
||||||
|
cat >"$config_path/scripts/panama-home-assistant" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
catalog)
|
||||||
|
printf '%s\n' '{"ok":false,"error":"test-helper"}'
|
||||||
|
;;
|
||||||
|
toggle|brightness)
|
||||||
|
printf '%s\n' '{"ok":true}'
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
EOF
|
||||||
|
chmod +x "$config_path/scripts/panama-home-assistant"
|
||||||
|
|
||||||
|
cat >"$test_bin/hyprctl" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "-j" && "${2:-}" == "monitors" ]]; then
|
||||||
|
printf '%s\n' '[{"focused":true,"name":"TEST-1","description":"Home and Phone contract","width":1920,"height":1080,"refreshRate":60,"scale":1,"currentFormat":"XRGB8888","colorManagementPreset":"srgb","vrr":false}]'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ "${1:-}" == "keyword" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exec /usr/sbin/hyprctl "$@"
|
||||||
|
EOF
|
||||||
|
chmod +x "$test_bin/hyprctl"
|
||||||
|
|
||||||
|
cat >"$test_bin/flatpak" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
printf '%s\n' "$*" >>"$PANAMA_FLATPAK_LOG"
|
||||||
|
if [[ "${1:-}" == "info" && "${2:-}" == "app.bluebubbles.BlueBubbles" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exit 97
|
||||||
|
EOF
|
||||||
|
chmod +x "$test_bin/flatpak"
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf 'home phone settings contract: %s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_contains() {
|
||||||
|
local needle="$1"
|
||||||
|
local file="$2"
|
||||||
|
rg -Fq "$needle" "$file" || fail "$file is missing: $needle"
|
||||||
|
}
|
||||||
|
|
||||||
|
home_page="$repo_dir/config/dot/quickshell/modules/settings/HomePhonePage.qml"
|
||||||
|
favorite_card="$repo_dir/config/dot/quickshell/modules/settings/HomeFavoriteCard.qml"
|
||||||
|
available_row="$repo_dir/config/dot/quickshell/modules/settings/AvailableLightRow.qml"
|
||||||
|
system_settings="$repo_dir/config/dot/quickshell/services/SystemSettings.qml"
|
||||||
|
|
||||||
|
[[ -f "$home_page" ]] || fail 'HomePhonePage.qml is missing'
|
||||||
|
[[ -f "$favorite_card" ]] || fail 'HomeFavoriteCard.qml is missing'
|
||||||
|
[[ -f "$available_row" ]] || fail 'AvailableLightRow.qml is missing'
|
||||||
|
assert_contains 'text: "Home & Phone"' "$home_page"
|
||||||
|
assert_contains 'text: "Choose what appears in Control Center and keep phone continuity close at hand."' "$home_page"
|
||||||
|
assert_contains 'Connected · ' "$home_page"
|
||||||
|
assert_contains 'Last update unavailable · showing saved controls' "$home_page"
|
||||||
|
assert_contains 'Authentication required' "$home_page"
|
||||||
|
assert_contains 'Home Assistant is not configured' "$home_page"
|
||||||
|
assert_contains 'HomePreferences.setAlias' "$home_page"
|
||||||
|
assert_contains 'HomePreferences.move' "$home_page"
|
||||||
|
assert_contains 'HomePreferences.remove' "$home_page"
|
||||||
|
assert_contains 'HomePreferences.add' "$home_page"
|
||||||
|
assert_contains 'HomePreferences.retrySave' "$home_page"
|
||||||
|
assert_contains 'Choose lights below to build your Control Center shelf.' "$home_page"
|
||||||
|
assert_contains 'All discovered lights are already selected' "$home_page"
|
||||||
|
assert_contains 'No lights match that search' "$home_page"
|
||||||
|
assert_contains 'Opens BlueBubbles' "$home_page"
|
||||||
|
[[ "$(rg -Fc 'required property var modelData' "$home_page")" -ge 2 ]] \
|
||||||
|
|| fail 'HomePhonePage.qml does not bind both reusable delegates to modelData'
|
||||||
|
if rg -Fq 'index: model.index' "$home_page"; then
|
||||||
|
fail 'HomePhonePage.qml reads an undefined model.index instead of the delegate index'
|
||||||
|
fi
|
||||||
|
if rg -qi 'token|bearer|api/states' "$home_page"; then
|
||||||
|
fail 'HomePhonePage.qml crosses the credential or REST privacy boundary'
|
||||||
|
fi
|
||||||
|
assert_contains 'signal aliasCommitted(string id, string alias)' "$favorite_card"
|
||||||
|
assert_contains 'signal removeRequested(string id)' "$favorite_card"
|
||||||
|
assert_contains 'signal moveRequested(string id, int targetIndex)' "$favorite_card"
|
||||||
|
assert_contains 'DragHandler {' "$favorite_card"
|
||||||
|
assert_contains 'onEditingFinished:' "$favorite_card"
|
||||||
|
assert_contains 'text: "Control Center"' "$favorite_card"
|
||||||
|
assert_contains 'activeFocusOnTab: true' "$favorite_card"
|
||||||
|
assert_contains 'signal addRequested(string id)' "$available_row"
|
||||||
|
assert_contains 'activeFocusOnTab: true' "$available_row"
|
||||||
|
assert_contains 'readonly property bool bluebubblesAvailable: root.bluebubblesDetected' "$system_settings"
|
||||||
|
assert_contains 'command: ["flatpak", "info", "app.bluebubbles.BlueBubbles"]' "$system_settings"
|
||||||
|
assert_contains '"bluebubbles": ["flatpak", "run", "app.bluebubbles.BlueBubbles"]' "$system_settings"
|
||||||
|
|
||||||
|
qs_for_test() {
|
||||||
|
PATH="$test_bin:$PATH" QS_CONFIG_PATH="$config_path" XDG_STATE_HOME="$state_home" \
|
||||||
|
PANAMA_FLATPAK_LOG="$flatpak_log" qs -p "$config_path" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_test_shell() {
|
||||||
|
qs_for_test kill >/dev/null 2>&1 || true
|
||||||
|
for _ in $(seq 1 80); do
|
||||||
|
if ! qs_for_test list 2>/dev/null | rg '^Instance ' >/dev/null \
|
||||||
|
&& ! qs_for_test ipc show >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
qs_for_test ipc call settings close >/dev/null 2>&1 || true
|
||||||
|
if stop_test_shell; then
|
||||||
|
rm -rf "$state_home"
|
||||||
|
else
|
||||||
|
printf 'home phone settings contract: branch shell did not stop; retained %s\n' \
|
||||||
|
"$state_home" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
start_test_shell() {
|
||||||
|
stop_test_shell || fail 'pre-existing branch shell did not stop cleanly'
|
||||||
|
for _attempt in 1 2; do
|
||||||
|
qs_for_test --daemonize >"$shell_log" 2>&1
|
||||||
|
for _ in $(seq 1 80); do
|
||||||
|
if qs_for_test ipc show 2>/dev/null | rg '^target settings-system$' >/dev/null; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
stop_test_shell || fail 'failed branch-shell attempt did not stop cleanly'
|
||||||
|
done
|
||||||
|
sed -n '1,240p' "$shell_log" >&2
|
||||||
|
fail 'isolated branch shell did not start'
|
||||||
|
}
|
||||||
|
|
||||||
|
start_test_shell
|
||||||
|
qs_for_test ipc call home-assistant fixture ready >/dev/null
|
||||||
|
qs_for_test ipc call settings page home-phone >/dev/null
|
||||||
|
|
||||||
|
status='{}'
|
||||||
|
for _ in $(seq 1 40); do
|
||||||
|
status="$(qs_for_test ipc call settings status | jq -c .)"
|
||||||
|
if jq -e '.page == "home-phone" and .discoveredCount == 7 and .selectedCount == 7' \
|
||||||
|
<<<"$status" >/dev/null; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
jq -e '.open == true and .page == "home-phone" and .discoveredCount == 7 and .selectedCount == 7' \
|
||||||
|
<<<"$status" >/dev/null || fail "Home & Phone diagnostics are incomplete: $status"
|
||||||
|
|
||||||
|
system_status="$(qs_for_test ipc call settings-system status | jq -c .)"
|
||||||
|
jq -e '.bluebubblesAvailable == true' <<<"$system_status" >/dev/null \
|
||||||
|
|| fail "BlueBubbles availability was not exposed: $system_status"
|
||||||
|
|
||||||
|
shell_pid="$(qs_for_test list | awk '/Process ID:/ { print $3; exit }')"
|
||||||
|
[[ "$shell_pid" =~ ^[0-9]+$ ]] || fail 'could not identify the branch shell process'
|
||||||
|
for _ in $(seq 1 40); do
|
||||||
|
if /usr/sbin/hyprctl -j clients | jq -e --argjson pid "$shell_pid" \
|
||||||
|
'[.[] | select(.pid == $pid and .title == "Panama Settings" and .floating == false)] | length == 1' >/dev/null; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
/usr/sbin/hyprctl -j clients | jq -e --argjson pid "$shell_pid" \
|
||||||
|
'[.[] | select(.pid == $pid and .title == "Panama Settings" and .floating == false)] | length == 1' >/dev/null \
|
||||||
|
|| fail 'the branch shell did not own exactly one tiled Panama Settings client'
|
||||||
|
|
||||||
|
if [[ -n "${PANAMA_TEST_SCREENSHOT_PATH:-}" ]]; then
|
||||||
|
geometry="$(/usr/sbin/hyprctl -j clients | jq -r --argjson pid "$shell_pid" \
|
||||||
|
'.[] | select(.pid == $pid and .title == "Panama Settings") | "\(.at[0]),\(.at[1]) \(.size[0])x\(.size[1])"')"
|
||||||
|
[[ -n "$geometry" ]] || fail 'could not resolve the Settings client geometry'
|
||||||
|
grim -g "$geometry" "$PANAMA_TEST_SCREENSHOT_PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rg -Fxq 'info app.bluebubbles.BlueBubbles' "$flatpak_log" \
|
||||||
|
|| fail 'the fixed BlueBubbles availability probe did not run'
|
||||||
|
if rg -q '^run ' "$flatpak_log"; then
|
||||||
|
fail 'the contract started BlueBubbles'
|
||||||
|
fi
|
||||||
|
if find "$state_home" -name panama-home.json -print -quit | rg -q .; then
|
||||||
|
fail 'the read-only fixture route wrote Home preferences'
|
||||||
|
fi
|
||||||
|
|
||||||
|
trap - EXIT
|
||||||
|
cleanup
|
||||||
|
[[ ! -e "$state_home" ]] || fail 'temporary Home & Phone state was not removed after shell exit'
|
||||||
|
printf 'home phone settings contract: PASS\n'
|
||||||
@@ -2,34 +2,135 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
state_home="$(mktemp -d /tmp/panama-settings-pages-state.XXXXXX)"
|
||||||
|
source_config_path="$repo_dir/config/dot/quickshell"
|
||||||
|
config_path="$state_home/quickshell"
|
||||||
|
test_bin="$state_home/bin"
|
||||||
|
shell_log="$state_home/quickshell.log"
|
||||||
|
|
||||||
|
cleanup_bootstrap() {
|
||||||
|
rm -rf "$state_home"
|
||||||
|
}
|
||||||
|
trap cleanup_bootstrap EXIT
|
||||||
|
|
||||||
|
mkdir -p "$test_bin"
|
||||||
|
cp -a "$source_config_path" "$config_path"
|
||||||
|
|
||||||
|
cat >"$config_path/scripts/panama-home-assistant" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
catalog)
|
||||||
|
printf '%s\n' '{"ok":false,"error":"test-helper"}'
|
||||||
|
;;
|
||||||
|
toggle|brightness)
|
||||||
|
printf '%s\n' '{"ok":true}'
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
EOF
|
||||||
|
chmod +x "$config_path/scripts/panama-home-assistant"
|
||||||
|
|
||||||
|
cat >"$test_bin/hyprctl" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "-j" && "${2:-}" == "monitors" ]]; then
|
||||||
|
printf '%s\n' '[{"focused":true,"name":"TEST-1","description":"Settings contract","width":1920,"height":1080,"refreshRate":60,"scale":1,"currentFormat":"XRGB8888","colorManagementPreset":"srgb","vrr":false}]'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ "${1:-}" == "keyword" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exec /usr/sbin/hyprctl "$@"
|
||||||
|
EOF
|
||||||
|
chmod +x "$test_bin/hyprctl"
|
||||||
|
|
||||||
|
cat >"$test_bin/flatpak" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "info" ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exit 97
|
||||||
|
EOF
|
||||||
|
chmod +x "$test_bin/flatpak"
|
||||||
|
|
||||||
fail() {
|
fail() {
|
||||||
printf 'settings pages contract: %s\n' "$1" >&2
|
printf 'settings pages contract: %s\n' "$1" >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
qs_for_test() {
|
||||||
|
PATH="$test_bin:$PATH" QS_CONFIG_PATH="$config_path" XDG_STATE_HOME="$state_home" \
|
||||||
|
qs -p "$config_path" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_test_shell() {
|
||||||
|
qs_for_test kill >/dev/null 2>&1 || true
|
||||||
|
for _ in $(seq 1 80); do
|
||||||
|
if ! qs_for_test list 2>/dev/null | rg '^Instance ' >/dev/null \
|
||||||
|
&& ! qs_for_test ipc show >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
qs ipc call settings close >/dev/null 2>&1 || true
|
qs_for_test ipc call settings close >/dev/null 2>&1 || true
|
||||||
|
if stop_test_shell; then
|
||||||
|
rm -rf "$state_home"
|
||||||
|
else
|
||||||
|
printf 'settings pages contract: branch shell did not stop; retained %s\n' \
|
||||||
|
"$state_home" >&2
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
pages=(home appearance displays connectivity desktop sound notifications screen-intelligence shortcuts services about)
|
start_test_shell() {
|
||||||
|
stop_test_shell || fail 'pre-existing branch shell did not stop cleanly'
|
||||||
|
for _attempt in 1 2; do
|
||||||
|
qs_for_test --daemonize >"$shell_log" 2>&1
|
||||||
|
for _ in $(seq 1 80); do
|
||||||
|
if qs_for_test ipc show 2>/dev/null | rg '^target settings$' >/dev/null; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
stop_test_shell || fail 'failed branch-shell attempt did not stop cleanly'
|
||||||
|
done
|
||||||
|
sed -n '1,200p' "$shell_log" >&2
|
||||||
|
fail 'isolated branch shell did not start'
|
||||||
|
}
|
||||||
|
|
||||||
|
start_test_shell
|
||||||
|
qs_for_test ipc call home-assistant fixture ready >/dev/null
|
||||||
|
shell_pid="$(qs_for_test list | awk '/Process ID:/ { print $3; exit }')"
|
||||||
|
[[ "$shell_pid" =~ ^[0-9]+$ ]] || fail 'could not identify the branch shell process'
|
||||||
|
|
||||||
|
pages=(home appearance displays connectivity home-phone desktop sound notifications screen-intelligence shortcuts services about)
|
||||||
for page in "${pages[@]}"; do
|
for page in "${pages[@]}"; do
|
||||||
qs ipc call settings page "$page" >/dev/null
|
qs_for_test ipc call settings page "$page" >/dev/null
|
||||||
for _ in $(seq 1 20); do
|
for _ in $(seq 1 20); do
|
||||||
[[ "$(qs ipc call settings status | jq -r .page)" == "$page" ]] && break
|
[[ "$(qs_for_test ipc call settings status | jq -r .page)" == "$page" ]] && break
|
||||||
sleep 0.1
|
sleep 0.1
|
||||||
done
|
done
|
||||||
[[ "$(qs ipc call settings status | jq -r .page)" == "$page" ]] || fail "$page did not route"
|
[[ "$(qs_for_test ipc call settings status | jq -r .page)" == "$page" ]] || fail "$page did not route"
|
||||||
hyprctl -j clients | jq -e '[.[] | select(.title == "Panama Settings")] | length == 1' >/dev/null \
|
/usr/sbin/hyprctl -j clients | jq -e --argjson pid "$shell_pid" \
|
||||||
|| fail "$page created a missing or duplicate Settings window"
|
'[.[] | select(.pid == $pid and .title == "Panama Settings" and .floating == false)] | length == 1' >/dev/null \
|
||||||
|
|| fail "$page created a missing, floating, or duplicate Settings window"
|
||||||
done
|
done
|
||||||
|
|
||||||
qs ipc call settings page '__unsupported__' >/dev/null
|
qs_for_test ipc call settings page '__unsupported__' >/dev/null
|
||||||
[[ "$(qs ipc call settings status | jq -r .page)" == "home" ]] || fail 'unsupported page did not fall back to Home'
|
[[ "$(qs_for_test ipc call settings status | jq -r .page)" == "home" ]] || fail 'unsupported page did not fall back to Home'
|
||||||
|
|
||||||
hyprctl -j binds | jq -e '.[] | select(.description == "Panama Settings" and .key == "I" and .modmask == 64)' >/dev/null \
|
/usr/sbin/hyprctl -j binds | jq -e '.[] | select(.description == "Panama Settings" and .key == "I" and .modmask == 64)' >/dev/null \
|
||||||
|| fail 'Super+I is not registered as Panama Settings'
|
|| fail 'Super+I is not registered as Panama Settings'
|
||||||
hyprctl -j binds | jq -e '.[] | select(.description == "Screen Intelligence" and .key == "S" and .modmask == 65)' >/dev/null \
|
/usr/sbin/hyprctl -j binds | jq -e '.[] | select(.description == "Screen Intelligence" and .key == "S" and .modmask == 65)' >/dev/null \
|
||||||
|| fail 'Super+Shift+S is not registered as Screen Intelligence'
|
|| fail 'Super+Shift+S is not registered as Screen Intelligence'
|
||||||
|
|
||||||
desktop_file="$HOME/.local/share/applications/panama-settings.desktop"
|
desktop_file="$HOME/.local/share/applications/panama-settings.desktop"
|
||||||
@@ -44,4 +145,5 @@ desktop-file-validate "$intelligence_desktop_file" >/dev/null || fail 'Screen In
|
|||||||
|
|
||||||
trap - EXIT
|
trap - EXIT
|
||||||
cleanup
|
cleanup
|
||||||
|
[[ ! -e "$state_home" ]] || fail 'temporary Settings state was not removed after shell exit'
|
||||||
printf 'settings pages contract: PASS\n'
|
printf 'settings pages contract: PASS\n'
|
||||||
|
|||||||
Reference in New Issue
Block a user