mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-07 14:05:38 -05:00
bluetooth: switch to native quickshell bluetooth service
removes dependency on bluetoothctl commands
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Bluetooth
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
@@ -9,410 +9,466 @@ Singleton {
|
||||
|
||||
property bool bluetoothEnabled: false
|
||||
property bool bluetoothAvailable: false
|
||||
property var bluetoothDevices: []
|
||||
property var availableDevices: []
|
||||
readonly property list<BluetoothDevice> bluetoothDevices: []
|
||||
readonly property list<BluetoothDevice> availableDevices: []
|
||||
property bool scanning: false
|
||||
property bool discoverable: false
|
||||
|
||||
// Real Bluetooth Management
|
||||
Process {
|
||||
id: bluetoothStatusChecker
|
||||
command: ["bluetoothctl", "show"] // Use default controller
|
||||
running: true
|
||||
property var connectingDevices: ({})
|
||||
|
||||
Component.onCompleted: {
|
||||
refreshBluetoothState()
|
||||
updateDevices()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Bluetooth
|
||||
|
||||
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
|
||||
function onDefaultAdapterChanged() {
|
||||
console.log("BluetoothService: Default adapter changed")
|
||||
refreshBluetoothState()
|
||||
updateDevices()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Bluetooth.defaultAdapter
|
||||
|
||||
function onEnabledChanged() {
|
||||
refreshBluetoothState()
|
||||
updateDevices()
|
||||
}
|
||||
|
||||
function onDiscoveringChanged() {
|
||||
refreshBluetoothState()
|
||||
updateDevices()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.devices : null
|
||||
|
||||
function onModelReset() {
|
||||
updateDevices()
|
||||
}
|
||||
|
||||
function onItemAdded() {
|
||||
updateDevices()
|
||||
}
|
||||
|
||||
function onItemRemoved() {
|
||||
updateDevices()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Bluetooth.devices
|
||||
|
||||
function onModelReset() {
|
||||
updateDevices()
|
||||
}
|
||||
|
||||
function onItemAdded() {
|
||||
updateDevices()
|
||||
}
|
||||
|
||||
function onItemRemoved() {
|
||||
updateDevices()
|
||||
}
|
||||
}
|
||||
|
||||
function refreshBluetoothState() {
|
||||
root.bluetoothAvailable = Bluetooth.defaultAdapter !== null
|
||||
root.bluetoothEnabled = Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.enabled : false
|
||||
root.scanning = Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.discovering : false
|
||||
root.discoverable = Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.discoverable : false
|
||||
}
|
||||
|
||||
function updateDevices() {
|
||||
if (!Bluetooth.defaultAdapter) {
|
||||
clearDeviceList(root.bluetoothDevices)
|
||||
clearDeviceList(root.availableDevices)
|
||||
root.bluetoothDevices = []
|
||||
root.availableDevices = []
|
||||
return
|
||||
}
|
||||
|
||||
let newPairedDevices = []
|
||||
let newAvailableDevices = []
|
||||
let allNativeDevices = []
|
||||
|
||||
let adapterDevices = Bluetooth.defaultAdapter.devices
|
||||
if (adapterDevices.values) {
|
||||
allNativeDevices = allNativeDevices.concat(adapterDevices.values)
|
||||
}
|
||||
|
||||
if (Bluetooth.devices.values) {
|
||||
for (let device of Bluetooth.devices.values) {
|
||||
if (!allNativeDevices.some(d => d.address === device.address)) {
|
||||
allNativeDevices.push(device)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let device of allNativeDevices) {
|
||||
if (!device) continue
|
||||
|
||||
let deviceType = getDeviceType(device.name || device.deviceName, device.icon)
|
||||
let displayName = device.name || device.deviceName
|
||||
|
||||
if (!displayName || displayName.startsWith('/org/bluez') || displayName.includes('hci0') || displayName.length < 2) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (device.paired) {
|
||||
let existingDevice = findDeviceInList(root.bluetoothDevices, device.address)
|
||||
if (existingDevice) {
|
||||
updateDeviceData(existingDevice, device, deviceType, displayName)
|
||||
newPairedDevices.push(existingDevice)
|
||||
} else {
|
||||
root.bluetoothDevices = []
|
||||
let newDevice = createBluetoothDevice(device, deviceType, displayName)
|
||||
newPairedDevices.push(newDevice)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (Bluetooth.defaultAdapter.discovering && isDeviceDiscoverable(device)) {
|
||||
let existingDevice = findDeviceInList(root.availableDevices, device.address)
|
||||
if (existingDevice) {
|
||||
updateDeviceData(existingDevice, device, deviceType, displayName)
|
||||
newAvailableDevices.push(existingDevice)
|
||||
} else {
|
||||
let newDevice = createBluetoothDevice(device, deviceType, displayName)
|
||||
newAvailableDevices.push(newDevice)
|
||||
}
|
||||
|
||||
root.bluetoothDevices = devices
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cleanupOldDevices(root.bluetoothDevices, newPairedDevices)
|
||||
cleanupOldDevices(root.availableDevices, newAvailableDevices)
|
||||
|
||||
console.log("BluetoothService: Found", newPairedDevices.length, "paired devices and", newAvailableDevices.length, "available devices")
|
||||
|
||||
root.bluetoothDevices = newPairedDevices
|
||||
root.availableDevices = newAvailableDevices
|
||||
}
|
||||
|
||||
function createBluetoothDevice(nativeDevice, deviceType, displayName) {
|
||||
return deviceComponent.createObject(root, {
|
||||
mac: nativeDevice.address,
|
||||
name: displayName,
|
||||
type: deviceType,
|
||||
paired: nativeDevice.paired,
|
||||
connected: nativeDevice.connected,
|
||||
battery: nativeDevice.batteryAvailable ? Math.round(nativeDevice.battery * 100) : -1,
|
||||
signalStrength: nativeDevice.connected ? "excellent" : "unknown",
|
||||
canPair: !nativeDevice.paired,
|
||||
nativeDevice: nativeDevice,
|
||||
connecting: false,
|
||||
connectionFailed: false
|
||||
})
|
||||
}
|
||||
|
||||
function updateDeviceData(deviceObj, nativeDevice, deviceType, displayName) {
|
||||
deviceObj.name = displayName
|
||||
deviceObj.type = deviceType
|
||||
deviceObj.paired = nativeDevice.paired
|
||||
|
||||
// If device connected state changed, clear connecting/failed states
|
||||
if (deviceObj.connected !== nativeDevice.connected) {
|
||||
deviceObj.connecting = false
|
||||
deviceObj.connectionFailed = false
|
||||
}
|
||||
|
||||
deviceObj.connected = nativeDevice.connected
|
||||
deviceObj.battery = nativeDevice.batteryAvailable ? Math.round(nativeDevice.battery * 100) : -1
|
||||
deviceObj.signalStrength = nativeDevice.connected ? "excellent" : "unknown"
|
||||
deviceObj.canPair = !nativeDevice.paired
|
||||
deviceObj.nativeDevice = nativeDevice
|
||||
}
|
||||
|
||||
function findDeviceInList(deviceList, address) {
|
||||
for (let device of deviceList) {
|
||||
if (device.mac === address) {
|
||||
return device
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function cleanupOldDevices(oldList, newList) {
|
||||
for (let oldDevice of oldList) {
|
||||
if (!newList.includes(oldDevice)) {
|
||||
oldDevice.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scanDevices() {
|
||||
if (root.bluetoothEnabled && root.bluetoothAvailable) {
|
||||
bluetoothDeviceScanner.running = true
|
||||
function clearDeviceList(deviceList) {
|
||||
for (let device of deviceList) {
|
||||
device.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
function isDeviceDiscoverable(device) {
|
||||
let displayName = device.name || device.deviceName
|
||||
if (!displayName || displayName.length < 2) return false
|
||||
|
||||
if (displayName.startsWith('/org/bluez') || displayName.includes('hci0')) return false
|
||||
|
||||
let nameLower = displayName.toLowerCase()
|
||||
|
||||
if (nameLower.match(/^[0-9a-f]{2}[:-][0-9a-f]{2}[:-][0-9a-f]{2}/)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (displayName.length < 3) return false
|
||||
|
||||
if (nameLower.includes('iphone') || nameLower.includes('ipad') ||
|
||||
nameLower.includes('airpods') || nameLower.includes('samsung') ||
|
||||
nameLower.includes('galaxy') || nameLower.includes('pixel') ||
|
||||
nameLower.includes('headphone') || nameLower.includes('speaker') ||
|
||||
nameLower.includes('mouse') || nameLower.includes('keyboard') ||
|
||||
nameLower.includes('watch') || nameLower.includes('buds') ||
|
||||
nameLower.includes('android')) {
|
||||
return true
|
||||
}
|
||||
|
||||
return displayName.length >= 4 && !displayName.match(/^[A-Z0-9_-]+$/)
|
||||
}
|
||||
|
||||
function getDeviceType(name, icon) {
|
||||
if (!name && !icon) return "bluetooth"
|
||||
|
||||
let nameLower = (name || "").toLowerCase()
|
||||
let iconLower = (icon || "").toLowerCase()
|
||||
|
||||
if (iconLower.includes("audio") || iconLower.includes("headset") || iconLower.includes("headphone") ||
|
||||
nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") ||
|
||||
nameLower.includes("arctis") || nameLower.includes("audio")) return "headset"
|
||||
else if (iconLower.includes("input-mouse") || nameLower.includes("mouse")) return "mouse"
|
||||
else if (iconLower.includes("input-keyboard") || nameLower.includes("keyboard")) return "keyboard"
|
||||
else if (iconLower.includes("phone") || nameLower.includes("phone") || nameLower.includes("iphone") ||
|
||||
nameLower.includes("samsung") || nameLower.includes("android")) return "phone"
|
||||
else if (iconLower.includes("watch") || nameLower.includes("watch")) return "watch"
|
||||
else if (iconLower.includes("audio-speakers") || nameLower.includes("speaker")) return "speaker"
|
||||
else if (iconLower.includes("video-display") || nameLower.includes("tv") || nameLower.includes("display")) return "tv"
|
||||
|
||||
return "bluetooth"
|
||||
}
|
||||
|
||||
function startDiscovery() {
|
||||
root.scanning = true
|
||||
// Run comprehensive scan that gets all devices
|
||||
discoveryScanner.running = true
|
||||
if (Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.enabled) {
|
||||
Bluetooth.defaultAdapter.discovering = true
|
||||
updateDevices()
|
||||
}
|
||||
}
|
||||
|
||||
function stopDiscovery() {
|
||||
let stopDiscoveryProcess = Qt.createQmlObject('
|
||||
import Quickshell.Io
|
||||
Process {
|
||||
command: ["bluetoothctl", "scan", "off"]
|
||||
running: true
|
||||
onExited: {
|
||||
root.scanning = false
|
||||
}
|
||||
}
|
||||
', root)
|
||||
if (Bluetooth.defaultAdapter) {
|
||||
Bluetooth.defaultAdapter.discovering = false
|
||||
updateDevices()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
let device = findDeviceByMac(mac)
|
||||
if (device) {
|
||||
device.pair()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
let device = findDeviceByMac(mac)
|
||||
if (device) {
|
||||
device.connect()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
let device = findDeviceByMac(mac)
|
||||
if (device) {
|
||||
device.forget()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
let typedDevice = findDeviceInList(root.bluetoothDevices, mac)
|
||||
if (!typedDevice) {
|
||||
typedDevice = findDeviceInList(root.availableDevices, mac)
|
||||
}
|
||||
|
||||
if (typedDevice && typedDevice.nativeDevice) {
|
||||
if (typedDevice.connected) {
|
||||
console.log("Disconnecting device:", mac)
|
||||
typedDevice.connecting = false
|
||||
typedDevice.connectionFailed = false
|
||||
typedDevice.nativeDevice.connected = false
|
||||
} else {
|
||||
console.log("Connecting to device:", mac)
|
||||
typedDevice.connecting = true
|
||||
typedDevice.connectionFailed = false
|
||||
|
||||
// Set a timeout to handle connection failure
|
||||
Qt.callLater(() => {
|
||||
connectionTimeout.deviceMac = mac
|
||||
connectionTimeout.start()
|
||||
})
|
||||
|
||||
typedDevice.nativeDevice.connected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if (Bluetooth.defaultAdapter) {
|
||||
Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter.enabled
|
||||
}
|
||||
}
|
||||
|
||||
function findDeviceByMac(mac) {
|
||||
let typedDevice = findDeviceInList(root.bluetoothDevices, mac)
|
||||
if (typedDevice && typedDevice.nativeDevice) {
|
||||
return typedDevice.nativeDevice
|
||||
}
|
||||
|
||||
typedDevice = findDeviceInList(root.availableDevices, mac)
|
||||
if (typedDevice && typedDevice.nativeDevice) {
|
||||
return typedDevice.nativeDevice
|
||||
}
|
||||
|
||||
if (Bluetooth.defaultAdapter) {
|
||||
let adapterDevices = Bluetooth.defaultAdapter.devices
|
||||
if (adapterDevices.values) {
|
||||
for (let device of adapterDevices.values) {
|
||||
if (device && device.address === mac) {
|
||||
return device
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Bluetooth.devices.values) {
|
||||
for (let device of Bluetooth.devices.values) {
|
||||
if (device && device.address === mac) {
|
||||
return device
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
Timer {
|
||||
id: bluetoothMonitorTimer
|
||||
interval: 5000
|
||||
running: false; repeat: true
|
||||
interval: 2000
|
||||
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
|
||||
}
|
||||
updateDevices()
|
||||
}
|
||||
}
|
||||
|
||||
function enableMonitoring(enabled) {
|
||||
bluetoothMonitorTimer.running = enabled
|
||||
if (enabled) {
|
||||
// Immediately update when enabled
|
||||
bluetoothStatusChecker.running = true
|
||||
refreshBluetoothState()
|
||||
updateDevices()
|
||||
}
|
||||
}
|
||||
|
||||
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, ")")
|
||||
Timer {
|
||||
id: bluetoothStateRefreshTimer
|
||||
interval: 5000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
refreshBluetoothState()
|
||||
}
|
||||
|
||||
// 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"]
|
||||
Timer {
|
||||
id: connectionTimeout
|
||||
interval: 10000 // 10 second timeout
|
||||
running: false
|
||||
repeat: 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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
property string deviceMac: ""
|
||||
|
||||
onTriggered: {
|
||||
if (deviceMac) {
|
||||
let typedDevice = findDeviceInList(root.bluetoothDevices, deviceMac)
|
||||
if (!typedDevice) {
|
||||
typedDevice = findDeviceInList(root.availableDevices, deviceMac)
|
||||
}
|
||||
|
||||
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')
|
||||
if (typedDevice && typedDevice.connecting && !typedDevice.connected) {
|
||||
console.log("Connection timeout for device:", deviceMac)
|
||||
typedDevice.connecting = false
|
||||
typedDevice.connectionFailed = true
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clear failure state after 3 seconds
|
||||
Qt.callLater(() => {
|
||||
clearFailureTimer.deviceMac = deviceMac
|
||||
clearFailureTimer.start()
|
||||
})
|
||||
}
|
||||
deviceMac = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: clearFailureTimer
|
||||
interval: 3000
|
||||
running: false
|
||||
repeat: false
|
||||
|
||||
property string deviceMac: ""
|
||||
|
||||
onTriggered: {
|
||||
if (deviceMac) {
|
||||
let typedDevice = findDeviceInList(root.bluetoothDevices, deviceMac)
|
||||
if (!typedDevice) {
|
||||
typedDevice = findDeviceInList(root.availableDevices, deviceMac)
|
||||
}
|
||||
|
||||
root.availableDevices = allDevices
|
||||
root.scanning = false
|
||||
|
||||
if (typedDevice) {
|
||||
typedDevice.connectionFailed = false
|
||||
}
|
||||
deviceMac = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component BluetoothDevice: QtObject {
|
||||
required property string mac
|
||||
required property string name
|
||||
required property string type
|
||||
required property bool paired
|
||||
required property bool connected
|
||||
required property int battery
|
||||
required property string signalStrength
|
||||
required property bool canPair
|
||||
required property var nativeDevice // Reference to native Quickshell device
|
||||
|
||||
property bool connecting: false
|
||||
property bool connectionFailed: false
|
||||
|
||||
readonly property string displayName: name
|
||||
readonly property bool batteryAvailable: battery >= 0
|
||||
readonly property string connectionStatus: {
|
||||
if (connecting) return "Connecting..."
|
||||
if (connectionFailed) return "Connection Failed"
|
||||
if (connected) return "Connected"
|
||||
return "Disconnected"
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: deviceComponent
|
||||
BluetoothDevice {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user