1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-07 05:55:37 -05:00
Files
DankMaterialShell/Services/BluetoothService.qml
2025-07-14 14:36:08 -04:00

418 lines
18 KiB
QML

import QtQuick
import Quickshell
import Quickshell.Io
pragma Singleton
pragma ComponentBehavior: Bound
Singleton {
id: root
property bool bluetoothEnabled: false
property bool bluetoothAvailable: false
property var bluetoothDevices: []
property var availableDevices: []
property bool scanning: false
property bool discoverable: false
// Real Bluetooth Management
Process {
id: bluetoothStatusChecker
command: ["bluetoothctl", "show"] // Use default controller
running: true
stdout: StdioCollector {
onStreamFinished: {
root.bluetoothAvailable = text.trim() !== "" && !text.includes("No default controller")
root.bluetoothEnabled = text.includes("Powered: yes")
if (root.bluetoothEnabled && root.bluetoothAvailable) {
bluetoothDeviceScanner.running = true
} else {
root.bluetoothDevices = []
}
}
}
}
Process {
id: bluetoothDeviceScanner
command: ["bash", "-c", "bluetoothctl devices | while read -r line; do if [[ $line =~ Device\\ ([0-9A-F:]+)\\ (.+) ]]; then mac=\"${BASH_REMATCH[1]}\"; name=\"${BASH_REMATCH[2]}\"; if [[ ! $name =~ ^/org/bluez ]]; then info=$(bluetoothctl info $mac); connected=$(echo \"$info\" | grep -m1 'Connected:' | awk '{print $2}'); battery=$(echo \"$info\" | grep -m1 'Battery Percentage:' | grep -o '[0-9]\\+'); echo \"$mac|$name|$connected|${battery:-}\"; fi; fi; done"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
let devices = []
let lines = text.trim().split('\n')
for (let line of lines) {
if (line.trim()) {
let parts = line.split('|')
if (parts.length >= 3) {
let mac = parts[0].trim()
let name = parts[1].trim()
let connected = parts[2].trim() === 'yes'
let battery = parts[3] ? parseInt(parts[3]) : -1
// Skip if name is still a technical path
if (name.startsWith('/org/bluez') || name.includes('hci0')) {
continue
}
// Determine device type from name
let type = "bluetooth"
let nameLower = name.toLowerCase()
if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis")) type = "headset"
else if (nameLower.includes("mouse")) type = "mouse"
else if (nameLower.includes("keyboard")) type = "keyboard"
else if (nameLower.includes("phone") || nameLower.includes("iphone") || nameLower.includes("samsung")) type = "phone"
else if (nameLower.includes("watch")) type = "watch"
else if (nameLower.includes("speaker")) type = "speaker"
devices.push({
mac: mac,
name: name,
type: type,
connected: connected,
battery: battery
})
}
}
}
root.bluetoothDevices = devices
}
}
}
}
function scanDevices() {
if (root.bluetoothEnabled && root.bluetoothAvailable) {
bluetoothDeviceScanner.running = true
}
}
function startDiscovery() {
root.scanning = true
// Run comprehensive scan that gets all devices
discoveryScanner.running = true
}
function stopDiscovery() {
let stopDiscoveryProcess = Qt.createQmlObject('
import Quickshell.Io
Process {
command: ["bluetoothctl", "scan", "off"]
running: true
onExited: {
root.scanning = false
}
}
', root)
}
function pairDevice(mac) {
console.log("Pairing device:", mac)
let pairProcess = Qt.createQmlObject('
import Quickshell.Io
Process {
command: ["bluetoothctl", "pair", "' + mac + '"]
running: true
onExited: (exitCode) => {
if (exitCode === 0) {
console.log("Pairing successful")
connectDevice("' + mac + '")
} else {
console.warn("Pairing failed with exit code:", exitCode)
}
availableDeviceScanner.running = true
bluetoothDeviceScanner.running = true
}
}
', root)
}
function connectDevice(mac) {
console.log("Connecting to device:", mac)
let connectProcess = Qt.createQmlObject('
import Quickshell.Io
Process {
command: ["bluetoothctl", "connect", "' + mac + '"]
running: true
onExited: (exitCode) => {
if (exitCode === 0) {
console.log("Connection successful")
} else {
console.warn("Connection failed with exit code:", exitCode)
}
bluetoothDeviceScanner.running = true
}
}
', root)
}
function removeDevice(mac) {
console.log("Removing device:", mac)
let removeProcess = Qt.createQmlObject('
import Quickshell.Io
Process {
command: ["bluetoothctl", "remove", "' + mac + '"]
running: true
onExited: {
bluetoothDeviceScanner.running = true
availableDeviceScanner.running = true
}
}
', root)
}
function toggleBluetoothDevice(mac) {
let device = root.bluetoothDevices.find(d => d.mac === mac)
if (device) {
let action = device.connected ? "disconnect" : "connect"
let toggleProcess = Qt.createQmlObject('
import Quickshell.Io
Process {
command: ["bluetoothctl", "' + action + '", "' + mac + '"]
running: true
onExited: bluetoothDeviceScanner.running = true
}
', root)
}
}
function toggleBluetooth() {
let action = root.bluetoothEnabled ? "off" : "on"
let toggleProcess = Qt.createQmlObject('
import Quickshell.Io
Process {
command: ["bluetoothctl", "power", "' + action + '"]
running: true
onExited: bluetoothStatusChecker.running = true
}
', root)
}
Timer {
id: bluetoothMonitorTimer
interval: 5000
running: false; repeat: true
onTriggered: {
bluetoothStatusChecker.running = true
if (root.bluetoothEnabled) {
bluetoothDeviceScanner.running = true
// Also refresh paired devices to get current connection status
pairedDeviceChecker.discoveredToMerge = []
pairedDeviceChecker.running = true
}
}
}
function enableMonitoring(enabled) {
bluetoothMonitorTimer.running = enabled
if (enabled) {
// Immediately update when enabled
bluetoothStatusChecker.running = true
}
}
property var discoveredDevices: []
// Handle discovered devices
function _handleDiscovered(found) {
let discoveredDevices = []
for (let device of found) {
let type = "bluetooth"
let nameLower = device.name.toLowerCase()
if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis") || nameLower.includes("audio")) type = "headset"
else if (nameLower.includes("mouse")) type = "mouse"
else if (nameLower.includes("keyboard")) type = "keyboard"
else if (nameLower.includes("phone") || nameLower.includes("iphone") || nameLower.includes("samsung") || nameLower.includes("android")) type = "phone"
else if (nameLower.includes("watch")) type = "watch"
else if (nameLower.includes("speaker")) type = "speaker"
else if (nameLower.includes("tv") || nameLower.includes("display")) type = "tv"
discoveredDevices.push({
mac: device.mac,
name: device.name,
type: type,
paired: false,
connected: false,
rssi: -70,
signalStrength: "fair",
canPair: true
})
console.log(" -", device.name, "(", device.mac, ")")
}
// Get paired devices first, then merge with discovered
pairedDeviceChecker.discoveredToMerge = discoveredDevices
pairedDeviceChecker.running = true
}
// Get only currently connected/paired devices that matter
Process {
id: availableDeviceScanner
command: ["bash", "-c", "bluetoothctl devices | while read -r line; do if [[ $line =~ Device\\ ([A-F0-9:]+)\\ (.+) ]]; then mac=\"${BASH_REMATCH[1]}\"; name=\"${BASH_REMATCH[2]}\"; info=$(bluetoothctl info \"$mac\" 2>/dev/null); paired=$(echo \"$info\" | grep -m1 'Paired:' | awk '{print $2}'); connected=$(echo \"$info\" | grep -m1 'Connected:' | awk '{print $2}'); if [[ \"$paired\" == \"yes\" ]] || [[ \"$connected\" == \"yes\" ]]; then echo \"$mac|$name|$paired|$connected\"; fi; fi; done"]
running: false
stdout: StdioCollector {
onStreamFinished: {
let devices = []
if (text.trim()) {
let lines = text.trim().split('\n')
for (let line of lines) {
if (line.trim()) {
let parts = line.split('|')
if (parts.length >= 4) {
let mac = parts[0].trim()
let name = parts[1].trim()
let paired = parts[2].trim() === 'yes'
let connected = parts[3].trim() === 'yes'
// Skip technical names
if (name.startsWith('/org/bluez') || name.includes('hci0') || name.length < 3) {
continue
}
// Determine device type
let type = "bluetooth"
let nameLower = name.toLowerCase()
if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis") || nameLower.includes("audio")) type = "headset"
else if (nameLower.includes("mouse")) type = "mouse"
else if (nameLower.includes("keyboard")) type = "keyboard"
else if (nameLower.includes("phone") || nameLower.includes("iphone") || nameLower.includes("samsung") || nameLower.includes("android")) type = "phone"
else if (nameLower.includes("watch")) type = "watch"
else if (nameLower.includes("speaker")) type = "speaker"
else if (nameLower.includes("tv") || nameLower.includes("display")) type = "tv"
devices.push({
mac: mac,
name: name,
type: type,
paired: paired,
connected: connected,
rssi: 0,
signalStrength: "unknown",
canPair: false // Already paired
})
}
}
}
}
root.availableDevices = devices
}
}
}
// Discovery scanner using bluetoothctl --timeout
Process {
id: discoveryScanner
// Discover for 8 s in non-interactive mode, then auto-exit
command: ["bluetoothctl",
"--timeout", "8",
"--monitor", // keeps stdout unbuffered
"scan", "on"]
running: false
stdout: StdioCollector {
onStreamFinished: {
/*
* bluetoothctl prints lines like:
* [NEW] Device 12:34:56:78:9A:BC My-Headphones
*/
const rx = /^\[NEW\] Device ([0-9A-F:]+)\s+(.+)$/i;
const found = text.split('\n')
.filter(l => rx.test(l))
.map(l => {
const [,mac,name] = l.match(rx);
return { mac, name };
});
root._handleDiscovered(found);
}
}
onExited: {
root.scanning = false
}
}
// Get paired devices and merge with discovered ones
Process {
id: pairedDeviceChecker
command: ["bash", "-c", "bluetoothctl devices | while read -r line; do if [[ $line =~ Device\\ ([A-F0-9:]+)\\ (.+) ]]; then mac=\"${BASH_REMATCH[1]}\"; name=\"${BASH_REMATCH[2]}\"; if [[ ${#name} -gt 3 ]] && [[ ! $name =~ ^/org/bluez ]] && [[ ! $name =~ hci0 ]]; then info=$(bluetoothctl info \"$mac\" 2>/dev/null); paired=$(echo \"$info\" | grep -m1 'Paired:' | awk '{print $2}'); connected=$(echo \"$info\" | grep -m1 'Connected:' | awk '{print $2}'); echo \"$mac|$name|$paired|$connected\"; fi; fi; done"]
running: false
property var discoveredToMerge: []
stdout: StdioCollector {
onStreamFinished: {
// Start with discovered devices (unpaired, available to pair)
let allDevices = [...pairedDeviceChecker.discoveredToMerge]
let seenMacs = new Set(allDevices.map(d => d.mac))
// Add only actually paired devices from bluetoothctl
if (text.trim()) {
let lines = text.trim().split('\n')
for (let line of lines) {
if (line.trim()) {
let parts = line.split('|')
if (parts.length >= 4) {
let mac = parts[0].trim()
let name = parts[1].trim()
let paired = parts[2].trim() === 'yes'
let connected = parts[3].trim() === 'yes'
// Only include if actually paired
if (!paired) continue
// Check if already in discovered list
if (seenMacs.has(mac)) {
// Update existing device to show it's paired
let existing = allDevices.find(d => d.mac === mac)
if (existing) {
existing.paired = true
existing.connected = connected
existing.canPair = false
}
continue
}
// Add paired device not found during scan
let type = "bluetooth"
let nameLower = name.toLowerCase()
if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis") || nameLower.includes("audio")) type = "headset"
else if (nameLower.includes("mouse")) type = "mouse"
else if (nameLower.includes("keyboard")) type = "keyboard"
else if (nameLower.includes("phone") || nameLower.includes("iphone") || nameLower.includes("samsung") || nameLower.includes("android")) type = "phone"
else if (nameLower.includes("watch")) type = "watch"
else if (nameLower.includes("speaker")) type = "speaker"
else if (nameLower.includes("tv") || nameLower.includes("display")) type = "tv"
allDevices.push({
mac: mac,
name: name,
type: type,
paired: true,
connected: connected,
rssi: -100,
signalStrength: "unknown",
canPair: false
})
}
}
}
}
root.availableDevices = allDevices
root.scanning = false
}
}
}
}