extract BluetoothService singleton from BluetoothModule and BluetoothMenu
This commit is contained in:
parent
d646d9b0fe
commit
de35cf016c
4 changed files with 142 additions and 127 deletions
|
|
@ -1,6 +1,5 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
|
||||||
import "." as M
|
import "." as M
|
||||||
import "../services" as S
|
import "../services" as S
|
||||||
|
|
||||||
|
|
@ -19,7 +18,7 @@ M.HoverPanel {
|
||||||
Text {
|
Text {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "\uF011"
|
text: "\uF011"
|
||||||
color: menuWindow._btEnabled ? menuWindow.accentColor : S.Theme.base04
|
color: S.BluetoothService.enabled ? menuWindow.accentColor : S.Theme.base04
|
||||||
font.pixelSize: S.Theme.fontSize
|
font.pixelSize: S.Theme.fontSize
|
||||||
font.family: S.Theme.iconFontFamily
|
font.family: S.Theme.iconFontFamily
|
||||||
|
|
||||||
|
|
@ -35,78 +34,16 @@ M.HoverPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
TapHandler {
|
TapHandler {
|
||||||
onTapped: {
|
onTapped: S.BluetoothService.setPower(!S.BluetoothService.enabled)
|
||||||
powerProc._action = menuWindow._btEnabled ? "off" : "on";
|
|
||||||
powerProc.running = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onVisibleChanged: if (visible)
|
onVisibleChanged: if (visible)
|
||||||
scanner.running = true
|
S.BluetoothService.refresh()
|
||||||
|
|
||||||
property var _devices: []
|
|
||||||
property bool _btEnabled: true
|
|
||||||
|
|
||||||
property Process _scanner: Process {
|
|
||||||
id: scanner
|
|
||||||
running: false
|
|
||||||
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---");
|
|
||||||
menuWindow._btEnabled = (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);
|
|
||||||
});
|
|
||||||
menuWindow._devices = devs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property Process _powerProc: Process {
|
|
||||||
id: powerProc
|
|
||||||
property string _action: ""
|
|
||||||
command: ["bluetoothctl", "power", _action]
|
|
||||||
onRunningChanged: if (!running) {
|
|
||||||
scanner.running = true;
|
|
||||||
menuWindow.keepOpen(500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property Process _toggleProc: Process {
|
|
||||||
id: toggleProc
|
|
||||||
property string action: ""
|
|
||||||
property string mac: ""
|
|
||||||
command: ["bluetoothctl", action, mac]
|
|
||||||
onRunningChanged: if (!running) {
|
|
||||||
scanner.running = true;
|
|
||||||
menuWindow.keepOpen(500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: menuWindow._devices
|
model: S.BluetoothService.devices
|
||||||
|
|
||||||
delegate: Item {
|
delegate: Item {
|
||||||
id: entry
|
id: entry
|
||||||
|
|
@ -167,21 +104,20 @@ M.HoverPanel {
|
||||||
}
|
}
|
||||||
TapHandler {
|
TapHandler {
|
||||||
onTapped: {
|
onTapped: {
|
||||||
toggleProc.action = entry.modelData.connected ? "disconnect" : "connect";
|
S.BluetoothService.toggleDevice(entry.modelData.mac, !entry.modelData.connected);
|
||||||
toggleProc.mac = entry.modelData.mac;
|
menuWindow.keepOpen(500);
|
||||||
toggleProc.running = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
visible: menuWindow._devices.length === 0
|
visible: S.BluetoothService.devices.length === 0
|
||||||
width: menuWindow.contentWidth
|
width: menuWindow.contentWidth
|
||||||
height: 32
|
height: 32
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
text: menuWindow._btEnabled ? "No paired devices" : "Bluetooth is off"
|
text: S.BluetoothService.enabled ? "No paired devices" : "Bluetooth is off"
|
||||||
color: S.Theme.base04
|
color: S.Theme.base04
|
||||||
font.pixelSize: S.Theme.fontSize
|
font.pixelSize: S.Theme.fontSize
|
||||||
font.family: S.Theme.fontFamily
|
font.family: S.Theme.fontFamily
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,24 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
|
||||||
import "." as M
|
import "." as M
|
||||||
import "../services" as S
|
import "../services" as S
|
||||||
|
|
||||||
M.BarSection {
|
M.BarSection {
|
||||||
id: root
|
id: root
|
||||||
spacing: S.Theme.moduleSpacing
|
spacing: S.Theme.moduleSpacing
|
||||||
opacity: S.Modules.bluetooth.enable && root.state !== "unavailable" ? 1 : 0
|
opacity: S.Modules.bluetooth.enable && S.BluetoothService.state !== "unavailable" ? 1 : 0
|
||||||
visible: opacity > 0
|
visible: opacity > 0
|
||||||
tooltip: {
|
tooltip: {
|
||||||
if (root.state === "off")
|
if (S.BluetoothService.state === "off")
|
||||||
return "Bluetooth: off";
|
return "Bluetooth: off";
|
||||||
if (root.state === "connected")
|
if (S.BluetoothService.state === "connected")
|
||||||
return "Bluetooth: " + root.device + (root.batteryPct >= 0 ? "\nBattery: " + root.batteryPct + "%" : "");
|
return "Bluetooth: " + S.BluetoothService.device + (S.BluetoothService.batteryPct >= 0 ? "\nBattery: " + S.BluetoothService.batteryPct + "%" : "");
|
||||||
return "Bluetooth: on";
|
return "Bluetooth: on";
|
||||||
}
|
}
|
||||||
|
|
||||||
property string state: "unavailable"
|
|
||||||
property string device: ""
|
|
||||||
property int batteryPct: -1
|
|
||||||
|
|
||||||
function _parse(text) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: proc
|
|
||||||
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: root._parse(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Event-driven: watch BlueZ DBus property changes
|
|
||||||
Process {
|
|
||||||
id: btMonitor
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Timer {
|
|
||||||
id: _debounce
|
|
||||||
interval: 500
|
|
||||||
onTriggered: proc.running = true
|
|
||||||
}
|
|
||||||
Timer {
|
|
||||||
interval: 60000
|
|
||||||
running: S.Modules.bluetooth.enable
|
|
||||||
repeat: true
|
|
||||||
onTriggered: proc.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
M.BarIcon {
|
M.BarIcon {
|
||||||
icon: "\uF294"
|
icon: "\uF294"
|
||||||
color: root.state === "off" ? S.Theme.base04 : root.accentColor
|
color: S.BluetoothService.state === "off" ? S.Theme.base04 : root.accentColor
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
TapHandler {
|
TapHandler {
|
||||||
onTapped: {
|
onTapped: {
|
||||||
|
|
@ -76,8 +28,8 @@ M.BarSection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
M.BarLabel {
|
M.BarLabel {
|
||||||
visible: root.state === "connected"
|
visible: S.BluetoothService.state === "connected"
|
||||||
label: root.device + (root.batteryPct >= 0 ? " " + root.batteryPct + "%" : "")
|
label: S.BluetoothService.device + (S.BluetoothService.batteryPct >= 0 ? " " + S.BluetoothService.batteryPct + "%" : "")
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
TapHandler {
|
TapHandler {
|
||||||
onTapped: {
|
onTapped: {
|
||||||
|
|
|
||||||
126
shell/services/BluetoothService.qml
Normal file
126
shell/services/BluetoothService.qml
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,3 +10,4 @@ singleton LockService 1.0 LockService.qml
|
||||||
singleton BacklightService 1.0 BacklightService.qml
|
singleton BacklightService 1.0 BacklightService.qml
|
||||||
singleton MprisService 1.0 MprisService.qml
|
singleton MprisService 1.0 MprisService.qml
|
||||||
singleton NetworkService 1.0 NetworkService.qml
|
singleton NetworkService 1.0 NetworkService.qml
|
||||||
|
singleton BluetoothService 1.0 BluetoothService.qml
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue