bluetooth flyout: paired devices with connect/disconnect

This commit is contained in:
Damocles 2026-04-12 18:27:20 +02:00
parent a230b72344
commit fd71211176
4 changed files with 159 additions and 1 deletions

View file

@ -103,7 +103,7 @@ PanelWindow {
M.BarGroup { M.BarGroup {
borderColor: M.Theme.base0D borderColor: M.Theme.base0D
M.Network { bar: bar; visible: M.Modules.network.enable } M.Network { bar: bar; visible: M.Modules.network.enable }
M.Bluetooth {} M.Bluetooth { bar: bar }
} }
// Controls // Controls

View file

@ -1,4 +1,5 @@
import QtQuick import QtQuick
import Quickshell
import Quickshell.Io import Quickshell.Io
import "." as M import "." as M
@ -66,12 +67,29 @@ M.BarSection {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
required property var bar
TapHandler { TapHandler {
acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onTapped: menuLoader.active = !menuLoader.active
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: { onTapped: {
toggle.cmd = root.state === "off" ? "on" : "off"; toggle.cmd = root.state === "off" ? "on" : "off";
toggle.running = true; 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
}
}
} }

139
modules/BluetoothMenu.qml Normal file
View file

@ -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
}
}

View file

@ -24,6 +24,7 @@ MprisMenu 1.0 MprisMenu.qml
Network 1.0 Network.qml Network 1.0 Network.qml
NetworkMenu 1.0 NetworkMenu.qml NetworkMenu 1.0 NetworkMenu.qml
Bluetooth 1.0 Bluetooth.qml Bluetooth 1.0 Bluetooth.qml
BluetoothMenu 1.0 BluetoothMenu.qml
Backlight 1.0 Backlight.qml Backlight 1.0 Backlight.qml
Cpu 1.0 Cpu.qml Cpu 1.0 Cpu.qml
Memory 1.0 Memory.qml Memory 1.0 Memory.qml