From fbf729b25cd99d20d44fa173691d863284c76baa Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Mon, 17 Aug 2026 18:48:28 -0400 Subject: [PATCH] Add BlueBubbles to Phone controls --- .../modules/quicksettings/PhoneControls.qml | 33 +++++++++-- tests/quickshell/control-center-contract.sh | 12 ++++ tests/quickshell/phone-messages-contract.sh | 58 +++++++++++++++++++ 3 files changed, 98 insertions(+), 5 deletions(-) create mode 100755 tests/quickshell/phone-messages-contract.sh diff --git a/config/dot/quickshell/modules/quicksettings/PhoneControls.qml b/config/dot/quickshell/modules/quicksettings/PhoneControls.qml index 94a974b..9e539d3 100644 --- a/config/dot/quickshell/modules/quicksettings/PhoneControls.qml +++ b/config/dot/quickshell/modules/quicksettings/PhoneControls.qml @@ -10,11 +10,14 @@ Item { signal toggleExpanded readonly property var phone: KdeConnect.preferredPhone + // Keep the four slots stable. The KDE actions can become unavailable when + // the phone disconnects, but the layout should not shift beneath Messages. 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)) + { id: "ring", glyph: "\u{F009A}", label: "Ring" }, + { id: "messages", glyph: "\u{F0365}", label: "Messages" } + ] implicitHeight: content.implicitHeight @@ -120,9 +123,8 @@ Item { Grid { id: actionsGrid width: parent.width - columns: Math.max(1, root.actionModels.length) + columns: 4 spacing: 7 - visible: root.actionModels.length > 0 Repeater { model: root.actionModels @@ -160,10 +162,17 @@ Item { MouseArea { id: actionMouse anchors.fill: parent - enabled: KdeConnect.phoneReachable && !KdeConnect.transferActive + enabled: actionButton.modelData.id === "messages" + ? SystemSettings.bluebubblesAvailable + : KdeConnect.phoneReachable + && !KdeConnect.transferActive + && KdeConnect.supports(actionButton.modelData.id) hoverEnabled: true + activeFocusOnTab: enabled cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor onClicked: root.invoke(actionButton.modelData.id) + Keys.onReturnPressed: root.invoke(actionButton.modelData.id) + Keys.onSpacePressed: root.invoke(actionButton.modelData.id) } } } @@ -179,6 +188,16 @@ Item { font.pixelSize: Theme.fontSizeSmall } + Text { + width: parent.width + visible: !SystemSettings.bluebubblesAvailable + text: "BlueBubbles is not installed" + color: Theme.fgMuted + horizontalAlignment: Text.AlignHCenter + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + } + Text { width: parent.width visible: KdeConnect.lastError !== "" @@ -254,6 +273,10 @@ Item { KdeConnect.sendClipboard(); else if (action === "ring") KdeConnect.ring(); + else if (action === "messages") { + if (SystemSettings.bluebubblesAvailable && SystemSettings.openApplication("bluebubbles")) + ShellState.close(); + } } function localPath(selectedUrl: url): string { diff --git a/tests/quickshell/control-center-contract.sh b/tests/quickshell/control-center-contract.sh index 5d54027..7432660 100755 --- a/tests/quickshell/control-center-contract.sh +++ b/tests/quickshell/control-center-contract.sh @@ -90,6 +90,18 @@ for component in ControlSectionHeader HomeBrightnessSlider HomeControls HomeTile || fail "$component is missing" done +[[ "$(rg -c 'id: "(share|clipboard|ring|messages)"' "$quicksettings_path/PhoneControls.qml")" -eq 4 ]] \ + || fail 'Phone controls do not expose four stable action models' +rg -Fq 'columns: 4' "$quicksettings_path/PhoneControls.qml" \ + || fail 'Phone actions are not arranged in four equal columns' +if rg -Fq '.filter(' "$quicksettings_path/PhoneControls.qml"; then + fail 'Phone action columns change when a KDE capability is unavailable' +fi +rg -Fq 'SystemSettings.bluebubblesAvailable' "$quicksettings_path/PhoneControls.qml" \ + || fail 'Messages action is not independently enabled by BlueBubbles' +rg -Fq 'KdeConnect.phoneReachable' "$quicksettings_path/PhoneControls.qml" \ + || fail 'KDE action reachability behavior is missing' + [[ -f "$quicksettings_path/qmldir" ]] \ || fail 'quick-settings module manifest is missing' rg -Fq 'HomeBrightnessSlider 1.0 HomeBrightnessSlider.qml' \ diff --git a/tests/quickshell/phone-messages-contract.sh b/tests/quickshell/phone-messages-contract.sh new file mode 100755 index 0000000..5c7f8fc --- /dev/null +++ b/tests/quickshell/phone-messages-contract.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +phone_controls="$project_root/config/dot/quickshell/modules/quicksettings/PhoneControls.qml" +system_settings="$project_root/config/dot/quickshell/services/SystemSettings.qml" + +fail() { + printf 'Phone Messages contract: %s\n' "$1" >&2 + exit 1 +} + +[[ -f "$phone_controls" ]] || fail 'Phone controls component is missing' +[[ -f "$system_settings" ]] || fail 'System settings service is missing' + +# This is a static contract only: it starts no shell, sends no IPC, and never +# evaluates the launch branch. Its only Flatpak use is source-text inspection. + +rg -Fq '"bluebubbles": ["flatpak", "run", "app.bluebubbles.BlueBubbles"]' "$system_settings" \ + || fail 'BlueBubbles does not use the fixed Flatpak argument vector' +rg -Fq 'readonly property bool bluebubblesAvailable: root.bluebubblesDetected' "$system_settings" \ + || fail 'BlueBubbles availability is not exposed independently' +rg -Fq 'command: ["flatpak", "info", "app.bluebubbles.BlueBubbles"]' "$system_settings" \ + || fail 'BlueBubbles installed-state probe is missing' + +for action in share clipboard ring messages; do + rg -Fq "id: \"$action\"" "$phone_controls" \ + || fail "Phone actions do not include $action" +done + +if rg -Fq '.filter(' "$phone_controls"; then + fail 'Phone actions are filtered instead of keeping four stable columns' +fi +rg -Fq 'columns: 4' "$phone_controls" \ + || fail 'Phone actions do not use four equal columns' +rg -Fq 'root.actionModels.length - 1' "$phone_controls" \ + || fail 'Phone action widths are not calculated from the fixed model count' +rg -Fq 'SystemSettings.bluebubblesAvailable' "$phone_controls" \ + || fail 'Messages enablement does not read BlueBubbles availability' +rg -Fq 'KdeConnect.phoneReachable' "$phone_controls" \ + || fail 'KDE action reachability semantics are missing' +rg -Fq 'KdeConnect.supports(actionButton.modelData.id)' "$phone_controls" \ + || fail 'KDE action capability semantics are missing' +rg -Fq 'else if (action === "messages")' "$phone_controls" \ + || fail 'Messages has no independent invocation branch' +rg -Fq 'SystemSettings.openApplication("bluebubbles")' "$phone_controls" \ + || fail 'Messages does not use the allow-listed BlueBubbles launcher' +rg -Fq 'BlueBubbles is not installed' "$phone_controls" \ + || fail 'Missing BlueBubbles has no quiet explanatory row' + +# A static negative guard keeps the Messages branch from accidentally inheriting +# KDE Connect reachability, transfer, or plugin conditions. +messages_branch="$(sed -n '/else if (action === "messages")/,/^ }/p' "$phone_controls")" +printf '%s\n' "$messages_branch" | rg -Fq 'KdeConnect.' \ + && fail 'Messages is coupled to KDE Connect' + +printf 'Phone Messages contract: PASS\n'