From 007393387aa5ea763dff2f6cc6c6e09caffdd2c2 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Thu, 28 May 2026 15:04:41 -0400 Subject: [PATCH] Fix Bluetooth panel device list handling --- shell/plugins/panels/bluetooth/Model.js | 17 +++++++++++++++-- shell/plugins/panels/bluetooth/Panel.qml | 13 ++++++------- test/shell.d/bluetooth-test.sh | 21 +++++++++++++++++++++ 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/shell/plugins/panels/bluetooth/Model.js b/shell/plugins/panels/bluetooth/Model.js index bb6f7184..c463b924 100644 --- a/shell/plugins/panels/bluetooth/Model.js +++ b/shell/plugins/panels/bluetooth/Model.js @@ -3,6 +3,18 @@ function deviceLabel(device) { return String(device.deviceName || device.name || "").trim() } +function toArray(values) { + if (!values) return [] + if (Array.isArray(values)) return values.slice() + + var length = Number(values.length || 0) + if (!isFinite(length) || length <= 0) return [] + + var list = [] + for (var i = 0; i < length; i++) list.push(values[i]) + return list +} + function isUuidLike(value) { var text = String(value || "").trim() if (text === "") return false @@ -23,13 +35,13 @@ function hasHumanName(device) { } function sortedByLabel(devices) { - var list = Array.isArray(devices) ? devices.slice() : [] + var list = toArray(devices) list.sort(function(a, b) { return deviceLabel(a).localeCompare(deviceLabel(b)) }) return list } function deviceLists(devices) { - var values = Array.isArray(devices) ? devices : [] + var values = toArray(devices) var connected = [] var known = [] var discovered = [] @@ -86,6 +98,7 @@ function sectionDevices(lists, section) { if (typeof module !== "undefined") { module.exports = { deviceLabel: deviceLabel, + toArray: toArray, isUuidLike: isUuidLike, isAddressLike: isAddressLike, hasHumanName: hasHumanName, diff --git a/shell/plugins/panels/bluetooth/Panel.qml b/shell/plugins/panels/bluetooth/Panel.qml index 9c17f4ac..9a3be5d0 100644 --- a/shell/plugins/panels/bluetooth/Panel.qml +++ b/shell/plugins/panels/bluetooth/Panel.qml @@ -219,9 +219,9 @@ Panel { function moveCursorH(delta) { if (!cursorActive) { cursorActive = true; return } - if (focusSection !== "known") return + if (focusSection !== "known" && focusSection !== "connected") return var dev = deviceAt(focusSection, selectedIndex) - if (!dev || dev.connected) return + if (!dev || !dev.address) return if (delta > 0) actionFocused = true else if (delta < 0) actionFocused = false } @@ -246,11 +246,10 @@ Panel { } } - // 'x' forgets remembered devices. Connected rows toggle connection via - // Enter/click, so the destructive forget action is intentionally unavailable - // while connected. + // 'x' forgets remembered devices. For connected devices this first + // disconnects, then removes the BlueZ pairing record via omarchy-bluetooth-device. function deleteSelected() { - if (focusSection !== "known") return + if (focusSection !== "known" && focusSection !== "connected") return var dev = deviceAt(focusSection, selectedIndex) if (!dev) return forgetDevice(dev) @@ -626,7 +625,7 @@ Panel { } readonly property bool rowSelected: root.cursorActive && root.focusSection === sectionName && root.selectedIndex === rowIndex - readonly property bool forgetAvailable: sectionName === "known" && !isConnected && !isDiscovered + readonly property bool forgetAvailable: (sectionName === "known" || sectionName === "connected") && !isDiscovered readonly property bool showForgetButton: forgetAvailable && (rowMouse.containsMouse || rowSelected) hasCursor: rowSelected && !root.actionFocused diff --git a/test/shell.d/bluetooth-test.sh b/test/shell.d/bluetooth-test.sh index f0123141..aa8c5f10 100644 --- a/test/shell.d/bluetooth-test.sh +++ b/test/shell.d/bluetooth-test.sh @@ -20,6 +20,17 @@ const devices = [ { name: 'Mouse', connected: false, trusted: true, address: '5' } ] +const arrayLikeDevices = { + 0: devices[0], + 1: devices[1], + length: 2 +} +assertDeepEqual( + bluetooth.toArray(arrayLikeDevices).map(bluetooth.deviceLabel), + ['Speaker', 'Headphones'], + 'bluetooth converts Quickshell QObjectList-style values into arrays' +) + const lists = bluetooth.deviceLists(devices) assertDeepEqual(lists.connected.map(bluetooth.deviceLabel), ['Headphones'], 'bluetooth groups connected devices') assertDeepEqual(lists.known.map(bluetooth.deviceLabel), ['Mouse', 'Speaker'], 'bluetooth groups known devices by label') @@ -27,6 +38,16 @@ assertDeepEqual(lists.discovered.map(bluetooth.deviceLabel), ['Keyboard'], 'blue assertDeepEqual(bluetooth.visibleSections(lists, true), ['connected', 'known', 'discovered'], 'bluetooth shows discovered section while scanning') assertDeepEqual(bluetooth.visibleSections(lists, false), ['connected', 'known'], 'bluetooth hides discovered section when not scanning') +const arrayLikeLists = bluetooth.deviceLists({ + 0: { name: 'Earbuds', connected: true, address: '6' }, + 1: { name: 'Trackpad', paired: true, address: '7' }, + 2: { name: 'Gamepad', address: '8' }, + length: 3 +}) +assertDeepEqual(arrayLikeLists.connected.map(bluetooth.deviceLabel), ['Earbuds'], 'bluetooth groups connected devices from array-like values') +assertDeepEqual(arrayLikeLists.known.map(bluetooth.deviceLabel), ['Trackpad'], 'bluetooth groups known devices from array-like values') +assertDeepEqual(arrayLikeLists.discovered.map(bluetooth.deviceLabel), ['Gamepad'], 'bluetooth groups discovered devices from array-like values') + assertDeepEqual( bluetooth.withPendingAction({ a: 'connecting' }, 'b', 'forgetting'), { a: 'connecting', b: 'forgetting' },