import QtQuick import Quickshell import Quickshell.Io import "." as M M.PopupPanel { id: menuWindow panelWidth: 250 property var _devices: [] property Process _scanner: Process { id: scanner running: true command: ["sh", "-c", "bluetoothctl devices Paired 2>/dev/null | while read -r _ mac name; do " + "info=$(bluetoothctl info \"$mac\" 2>/dev/null); " + "conn=$(echo \"$info\" | grep -c 'Connected: yes'); " + "bat=$(echo \"$info\" | awk -F'[(): ]' '/Battery Percentage/{for(i=1;i<=NF;i++) if($i+0==$i && $i!=\"\") print $i}'); " + "echo \"$mac:$conn:${bat:-}:$name\"; " + "done"] stdout: StdioCollector { onStreamFinished: { const devs = []; for (const line of text.trim().split("\n")) { if (!line) continue; const i1 = line.indexOf(":"); const i2 = line.indexOf(":", i1 + 1); const i3 = line.indexOf(":", i2 + 1); if (i3 < 0) continue; devs.push({ mac: line.slice(0, i1), connected: line.slice(i1 + 1, i2) === "1", battery: parseInt(line.slice(i2 + 1, i3)) || -1, name: line.slice(i3 + 1) }); } devs.sort((a, b) => { if (a.connected !== b.connected) return a.connected ? -1 : 1; return a.name.localeCompare(b.name); }); menuWindow._devices = devs; } } } property Process _toggleProc: Process { id: toggleProc property string action: "" property string mac: "" command: ["bluetoothctl", action, mac] onRunningChanged: if (!running) scanner.running = true } Repeater { model: menuWindow._devices delegate: Item { id: entry required property var modelData required property int index width: menuWindow.panelWidth height: 32 Rectangle { anchors.fill: parent anchors.leftMargin: 4 anchors.rightMargin: 4 color: entryArea.containsMouse ? M.Theme.base02 : "transparent" radius: M.Theme.radius } Text { id: btIcon anchors.left: parent.left anchors.leftMargin: 12 anchors.verticalCenter: parent.verticalCenter text: "\uF294" color: entry.modelData.connected ? M.Theme.base0D : M.Theme.base04 font.pixelSize: M.Theme.fontSize + 1 font.family: M.Theme.iconFontFamily } Text { anchors.left: btIcon.right anchors.leftMargin: 8 anchors.right: batLabel.left anchors.rightMargin: 4 anchors.verticalCenter: parent.verticalCenter text: entry.modelData.name color: entry.modelData.connected ? M.Theme.base0D : M.Theme.base05 font.pixelSize: M.Theme.fontSize font.family: M.Theme.fontFamily font.bold: entry.modelData.connected elide: Text.ElideRight } Text { id: batLabel anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter text: entry.modelData.battery >= 0 ? entry.modelData.battery + "%" : "" color: M.Theme.base04 font.pixelSize: M.Theme.fontSize - 1 font.family: M.Theme.fontFamily width: entry.modelData.battery >= 0 ? implicitWidth : 0 } MouseArea { id: entryArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { toggleProc.action = entry.modelData.connected ? "disconnect" : "connect"; toggleProc.mac = entry.modelData.mac; toggleProc.running = true; menuWindow.dismiss(); } } } } Text { visible: menuWindow._devices.length === 0 width: menuWindow.panelWidth height: 32 horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter text: "No paired devices" color: M.Theme.base04 font.pixelSize: M.Theme.fontSize font.family: M.Theme.fontFamily } }