pragma Singleton import QtQuick import Quickshell.Io import "." as S import NovaStats as NS 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; } // MAC address of device currently connecting/disconnecting property string pendingMac: "" function toggleDevice(mac, connect) { pendingMac = mac; _toggleProc._action = connect ? "connect" : "disconnect"; _toggleProc._mac = mac; _toggleProc.running = true; } // Status polling (bar icon state) property Process _statusProc: Process { running: NS.ModulesService.bluetoothEnable 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: NS.ModulesService.bluetoothEnable 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: NS.ModulesService.bluetoothEnable 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.pendingMac = ""; root.refresh(); } } }