nova-shell/shell/services/BluetoothService.qml

126 lines
4.9 KiB
QML

pragma Singleton
import QtQuick
import Quickshell.Io
import "." as S
QtObject {
id: root
// Adapter state: "unavailable" | "off" | "on" | "connected"
property string state: "unavailable"
property string device: ""
property int batteryPct: -1
// Paired device list and power state (for menu)
property var devices: []
property bool enabled: true
function refresh() {
_statusProc.running = true;
_scannerProc.running = true;
}
function setPower(on) {
_powerProc._action = on ? "on" : "off";
_powerProc.running = true;
}
function toggleDevice(mac, connect) {
_toggleProc._action = connect ? "connect" : "disconnect";
_toggleProc._mac = mac;
_toggleProc.running = true;
}
// Status polling (bar icon state)
property Process _statusProc: Process {
running: S.Modules.bluetooth.enable
command: ["sh", "-c", "s=$(bluetoothctl show 2>/dev/null); " + "[ -z \"$s\" ] && echo unavailable && exit; " + "echo \"$s\" | grep -q 'Powered: yes' || { echo off:; exit; }; " + "info=$(bluetoothctl info 2>/dev/null); " + "d=$(echo \"$info\" | awk -F': ' '/\\tName:/{n=$2}/Connected: yes/{c=1}END{if(c)print n}'); " + "[ -n \"$d\" ] && echo \"connected:$d\" || { echo on:; exit; }; " + "bat=$(echo \"$info\" | awk -F': ' '/Battery Percentage.*\\(/{gsub(/[^0-9]/,\"\",$2);print $2}'); " + "[ -n \"$bat\" ] && echo \"bat:$bat\""]
stdout: StdioCollector {
onStreamFinished: {
const lines = text.trim().split("\n");
const t = lines[0] || "";
const sep = t.indexOf(":");
root.state = sep === -1 ? t : t.slice(0, sep);
root.device = sep === -1 ? "" : t.slice(sep + 1);
root.batteryPct = -1;
for (let i = 1; i < lines.length; i++) {
if (lines[i].startsWith("bat:"))
root.batteryPct = parseInt(lines[i].slice(4)) || -1;
}
}
}
}
// Event-driven: watch BlueZ DBus property changes
property Process _monitor: Process {
running: S.Modules.bluetooth.enable
command: ["sh", "-c", "dbus-monitor --system \"interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',path_namespace='/org/bluez'\" 2>/dev/null"]
stdout: SplitParser {
splitMarker: "\n"
onRead: _debounce.restart()
}
}
property Timer _debounce: Timer {
interval: 500
onTriggered: root.refresh()
}
property Timer _fallbackPoll: Timer {
interval: 60000
running: S.Modules.bluetooth.enable
repeat: true
onTriggered: root.refresh()
}
// Paired device scanner (for menu)
property Process _scannerProc: Process {
command: ["sh", "-c", "bluetoothctl show 2>/dev/null | awk '/Powered:/{print $2; exit}';" + "echo '---DEVICES---';" + "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 sections = text.split("---DEVICES---");
root.enabled = (sections[0] || "").trim() === "yes";
const devs = [];
for (const line of (sections[1] || "").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);
});
root.devices = devs;
}
}
}
// Action processes
property Process _powerProc: Process {
property string _action: ""
command: ["bluetoothctl", "power", _action]
onRunningChanged: if (!running)
root.refresh()
}
property Process _toggleProc: Process {
property string _action: ""
property string _mac: ""
command: ["bluetoothctl", _action, _mac]
onRunningChanged: if (!running)
root.refresh()
}
}