diff --git a/modules/Bar.qml b/modules/Bar.qml index 8ab91e0..44f0fce 100644 --- a/modules/Bar.qml +++ b/modules/Bar.qml @@ -103,7 +103,7 @@ PanelWindow { M.BarGroup { borderColor: M.Theme.base0D M.Network { bar: bar; visible: M.Modules.network.enable } - M.Bluetooth {} + M.Bluetooth { bar: bar } } // Controls diff --git a/modules/Bluetooth.qml b/modules/Bluetooth.qml index 2f7812a..562962f 100644 --- a/modules/Bluetooth.qml +++ b/modules/Bluetooth.qml @@ -1,4 +1,5 @@ import QtQuick +import Quickshell import Quickshell.Io import "." as M @@ -66,12 +67,29 @@ M.BarSection { anchors.verticalCenter: parent.verticalCenter } + required property var bar + TapHandler { + acceptedButtons: Qt.LeftButton cursorShape: Qt.PointingHandCursor + onTapped: menuLoader.active = !menuLoader.active + } + + TapHandler { + acceptedButtons: Qt.RightButton onTapped: { toggle.cmd = root.state === "off" ? "on" : "off"; toggle.running = true; } } + Loader { + id: menuLoader + active: false + sourceComponent: M.BluetoothMenu { + screen: root.bar.screen + anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) + onDismissed: menuLoader.active = false + } + } } diff --git a/modules/BluetoothMenu.qml b/modules/BluetoothMenu.qml new file mode 100644 index 0000000..a4b7469 --- /dev/null +++ b/modules/BluetoothMenu.qml @@ -0,0 +1,139 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import "." as M + +M.PopupPanel { + id: menuWindow + + panelWidth: 250 + + property var _devices: [] + + 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; + } + } + } + + 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 + } +} diff --git a/modules/qmldir b/modules/qmldir index b179bb3..49b15cd 100644 --- a/modules/qmldir +++ b/modules/qmldir @@ -24,6 +24,7 @@ MprisMenu 1.0 MprisMenu.qml Network 1.0 Network.qml NetworkMenu 1.0 NetworkMenu.qml Bluetooth 1.0 Bluetooth.qml +BluetoothMenu 1.0 BluetoothMenu.qml Backlight 1.0 Backlight.qml Cpu 1.0 Cpu.qml Memory 1.0 Memory.qml