1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 05:25:41 -05:00

bluetooth: integrate with DMS API v9 - Supports proper pairing with an agent & pin, passcode, etc.

This commit is contained in:
bbedward
2025-10-23 11:54:22 -04:00
parent 61d68b1f76
commit 1311da7258
6 changed files with 482 additions and 14 deletions

View File

@@ -6,6 +6,7 @@ import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Bluetooth
import qs.Services
Singleton {
id: root
@@ -15,6 +16,7 @@ Singleton {
readonly property bool enabled: (adapter && adapter.enabled) ?? false
readonly property bool discovering: (adapter && adapter.discovering) ?? false
readonly property var devices: adapter ? adapter.devices : null
readonly property bool enhancedPairingAvailable: DMSService.dmsAvailable && DMSService.apiVersion >= 9 && DMSService.capabilities.includes("bluetooth")
readonly property bool connected: {
if (!adapter || !adapter.devices) {
return false
@@ -173,6 +175,25 @@ Singleton {
device.connect()
}
function pairDevice(device, callback) {
if (!device) {
if (callback) callback({error: "Invalid device"})
return
}
// The DMS backend actually implements a bluez agent, so we can pair anything
if (enhancedPairingAvailable) {
const devicePath = getDevicePath(device)
DMSService.bluetoothPair(devicePath, callback)
return
}
// Quickshell does not implement a bluez agent, so we can try to pair but only with devices that don't require a passcode
device.trusted = true
device.connect()
if (callback) callback({success: true})
}
function getCardName(device) {
if (!device) {
return ""
@@ -180,6 +201,14 @@ Singleton {
return `bluez_card.${device.address.replace(/:/g, "_")}`
}
function getDevicePath(device) {
if (!device || !device.address) {
return ""
}
const adapterPath = adapter ? "/org/bluez/hci0" : "/org/bluez/hci0"
return `${adapterPath}/dev_${device.address.replace(/:/g, "_")}`
}
function isAudioDevice(device) {
if (!device) {
return false

View File

@@ -42,6 +42,7 @@ Singleton {
signal loginctlEvent(var event)
signal capabilitiesReceived()
signal credentialsRequest(var data)
signal bluetoothPairingRequest(var data)
Component.onCompleted: {
if (socketPath && socketPath.length > 0) {
@@ -217,7 +218,10 @@ Singleton {
function sendSubscribeRequest() {
const request = {
"method": "subscribe"
"method": "subscribe",
"params": {
"services": ["bluetooth", "bluetooth.pairing"]
}
}
if (verboseLogs) {
@@ -270,6 +274,8 @@ Singleton {
} else {
loginctlStateUpdate(data)
}
} else if (service === "bluetooth.pairing") {
bluetoothPairingRequest(data)
}
}
@@ -408,4 +414,48 @@ Singleton {
function unlockSession(callback) {
sendRequest("loginctl.unlock", null, callback)
}
function bluetoothPair(devicePath, callback) {
sendRequest("bluetooth.pair", {
"device": devicePath
}, callback)
}
function bluetoothConnect(devicePath, callback) {
sendRequest("bluetooth.connect", {
"device": devicePath
}, callback)
}
function bluetoothDisconnect(devicePath, callback) {
sendRequest("bluetooth.disconnect", {
"device": devicePath
}, callback)
}
function bluetoothRemove(devicePath, callback) {
sendRequest("bluetooth.remove", {
"device": devicePath
}, callback)
}
function bluetoothTrust(devicePath, callback) {
sendRequest("bluetooth.trust", {
"device": devicePath
}, callback)
}
function bluetoothSubmitPairing(token, secrets, accept, callback) {
sendRequest("bluetooth.pairing.submit", {
"token": token,
"secrets": secrets,
"accept": accept
}, callback)
}
function bluetoothCancelPairing(token, callback) {
sendRequest("bluetooth.pairing.cancel", {
"token": token
}, callback)
}
}

View File

@@ -1,6 +1,6 @@
pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtCore
import QtQuick
@@ -640,7 +640,7 @@ Singleton {
const enrichedToplevel = {
"appId": bestMatch.appId,
"title": bestMatch.title,
"activated": bestMatch.activated,
"activated": niriWindow.is_focused ?? false,
"niriWindowId": niriWindow.id,
"niriWorkspaceId": niriWindow.workspace_id,
"activate": function () {
@@ -726,7 +726,7 @@ Singleton {
const enrichedToplevel = {
"appId": bestMatch.appId,
"title": bestMatch.title,
"activated": bestMatch.activated,
"activated": niriWindow.is_focused ?? false,
"niriWindowId": niriWindow.id,
"niriWorkspaceId": niriWindow.workspace_id,
"activate": function () {
@@ -753,12 +753,10 @@ Singleton {
}
function generateNiriLayoutConfig() {
const niriSocket = Quickshell.env("NIRI_SOCKET")
if (!niriSocket || niriSocket.length === 0)
return
if (configGenerationPending)
if (!CompositorService.isNiri || configGenerationPending)
return
suppressNextToast()
configGenerationPending = true
configGenerationDebounce.restart()
}