From 470b61f4e178042cbb446fd669cd2c00e74cf4c0 Mon Sep 17 00:00:00 2001 From: cbxcvl Date: Wed, 25 Feb 2026 23:50:16 -0300 Subject: [PATCH 1/3] feat(bluetooth): auto-connect paired & trusted devices with toggle control Adds automatic reconnection of paired and trusted Bluetooth devices when Bluetooth is enabled or when the shell starts. The feature is fully toggleable ON/OFF from three places: - Settings > Connections > Bluetooth (persistent NToggle) - Bluetooth Panel quick toggle (NIconButton in header) - IPC commands: toggleAutoConnect, enableAutoConnect, disableAutoConnect Changes: - New setting: bluetoothAutoConnect (default: true) - Auto-trust devices upon pairing via Instantiator/Connections watcher - 2s delay after BT enable to allow adapter initialization - Respects airplane mode - Toast notification when auto-connect fires --- Assets/Translations/en.json | 7 ++ Commons/Settings.qml | 1 + Modules/Panels/Bluetooth/BluetoothPanel.qml | 8 +++ .../Tabs/Connections/BluetoothSubTab.qml | 7 ++ Services/Control/IPCService.qml | 9 +++ Services/Networking/BluetoothService.qml | 70 +++++++++++++++++++ 6 files changed, 102 insertions(+) diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 138ec873c..8745b65f6 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -911,6 +911,8 @@ }, "connections": { "authentication-required": "Authentication required", + "bluetooth-auto-connect-description": "Automatically connect to trusted paired devices when Bluetooth is enabled.", + "bluetooth-auto-connect-label": "Auto-connect paired devices", "bluetooth-devices-unnamed": "Unnamed devices are not shown.", "bluetooth-discoverable": "This device is discoverable as {hostName} while this settings tab is open.", "bluetooth-rssi-polling-description": "Periodically sample RSSI for connected devices via bluetoothctl. May not be available for all devices; uses minimal resources when enabled.", @@ -1777,6 +1779,9 @@ }, "bluetooth": { "address-copied": "Address copied to clipboard", + "auto-connect-disabled": "Auto-connect disabled", + "auto-connect-enabled": "Auto-connect enabled", + "auto-connecting": "Connecting to {count} device(s)...", "confirm-code": "Confirm code {value} on the other device.", "connect-failed": "Failed to connect to device", "disconnect-failed": "Failed to disconnect from device", @@ -1849,6 +1854,8 @@ }, "tooltips": { "add-widget": "Add widget", + "bluetooth-auto-connect-off": "Auto-connect is off — click to enable", + "bluetooth-auto-connect-on": "Auto-connect is on — click to disable", "bluetooth-devices": "Bluetooth devices", "brightness-at": "Brightness: {brightness}%", "click-to-start-recording": "Screen recorder (start recording)", diff --git a/Commons/Settings.qml b/Commons/Settings.qml index a0904f0dd..6cda46076 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -559,6 +559,7 @@ Singleton { property string bluetoothDetailsViewMode: "grid" // "grid" or "list" property bool bluetoothHideUnnamedDevices: false property bool disableDiscoverability: false + property bool bluetoothAutoConnect: true } // session menu diff --git a/Modules/Panels/Bluetooth/BluetoothPanel.qml b/Modules/Panels/Bluetooth/BluetoothPanel.qml index c416829e6..92464d7b7 100644 --- a/Modules/Panels/Bluetooth/BluetoothPanel.qml +++ b/Modules/Panels/Bluetooth/BluetoothPanel.qml @@ -59,6 +59,14 @@ SmartPanel { baseSize: Style.baseWidgetSize * 0.65 } + NIconButton { + icon: Settings.data.network.bluetoothAutoConnect ? "bluetooth-connected" : "bluetooth" + tooltipText: Settings.data.network.bluetoothAutoConnect ? I18n.tr("tooltips.bluetooth-auto-connect-on") : I18n.tr("tooltips.bluetooth-auto-connect-off") + colorFg: Settings.data.network.bluetoothAutoConnect ? Color.mPrimary : Color.mOnSurfaceVariant + baseSize: Style.baseWidgetSize * 0.8 + onClicked: Settings.data.network.bluetoothAutoConnect = !Settings.data.network.bluetoothAutoConnect + } + NIconButton { icon: "settings" tooltipText: I18n.tr("tooltips.open-settings") diff --git a/Modules/Panels/Settings/Tabs/Connections/BluetoothSubTab.qml b/Modules/Panels/Settings/Tabs/Connections/BluetoothSubTab.qml index 4cf2cf75c..b2785eab7 100644 --- a/Modules/Panels/Settings/Tabs/Connections/BluetoothSubTab.qml +++ b/Modules/Panels/Settings/Tabs/Connections/BluetoothSubTab.qml @@ -313,6 +313,13 @@ Item { anchors.margins: Style.marginXL spacing: Style.marginM + NToggle { + label: I18n.tr("panels.connections.bluetooth-auto-connect-label") + description: I18n.tr("panels.connections.bluetooth-auto-connect-description") + checked: Settings.data.network.bluetoothAutoConnect + onToggled: checked => Settings.data.network.bluetoothAutoConnect = checked + } + NToggle { label: I18n.tr("panels.connections.hide-unnamed-devices-label") description: I18n.tr("panels.connections.hide-unnamed-devices-description") diff --git a/Services/Control/IPCService.qml b/Services/Control/IPCService.qml index c4898efe6..373cf5cba 100644 --- a/Services/Control/IPCService.qml +++ b/Services/Control/IPCService.qml @@ -606,6 +606,15 @@ Singleton { bluetoothPanel?.toggle(null, "Bluetooth"); }); } + function toggleAutoConnect() { + Settings.data.network.bluetoothAutoConnect = !Settings.data.network.bluetoothAutoConnect; + } + function enableAutoConnect() { + Settings.data.network.bluetoothAutoConnect = true; + } + function disableAutoConnect() { + Settings.data.network.bluetoothAutoConnect = false; + } } IpcHandler { diff --git a/Services/Networking/BluetoothService.qml b/Services/Networking/BluetoothService.qml index 875baae33..f97f8d0cb 100644 --- a/Services/Networking/BluetoothService.qml +++ b/Services/Networking/BluetoothService.qml @@ -67,6 +67,17 @@ Singleton { // Internal: temporarily pause discovery during pair/connect to reduce HCI churn property bool _discoveryWasRunning: false property bool _ctlInit: false + property bool _autoConnectInProgress: false + + // Mirror the setting so we can react to changes via onAutoConnectEnabledChanged + property bool autoConnectEnabled: Settings?.data?.network?.bluetoothAutoConnect ?? true + onAutoConnectEnabledChanged: { + if (autoConnectEnabled && adapter && adapter.enabled) { + autoConnectTimer.restart(); + } else { + autoConnectTimer.stop(); + } + } Timer { id: initDelayTimer @@ -75,6 +86,29 @@ Singleton { repeat: false } + Timer { + id: autoConnectTimer + interval: 2000 + repeat: false + onTriggered: root.attemptAutoConnect() + } + + Instantiator { + id: deviceWatcher + active: root.autoConnectEnabled && root.adapter !== null + model: root.adapter ? root.adapter.devices : null + delegate: Connections { + required property var modelData + target: modelData + function onPairedChanged() { + if (modelData.paired && !modelData.trusted) { + Logger.i("Bluetooth", "Auto-trusting newly paired device:", modelData.name || modelData.deviceName); + modelData.trusted = true; + } + } + } + } + function init() { Logger.i("Bluetooth", "Service started"); } @@ -86,6 +120,10 @@ Singleton { Quickshell.execDetached(["rfkill", "block", "wifi"]); Quickshell.execDetached(["rfkill", "block", "bluetooth"]); } + // Auto-connect on startup if BT is already enabled + if (root.autoConnectEnabled && adapter && adapter.enabled) { + autoConnectTimer.restart(); + } } // Handle system wakeup to force-poll and ensure state is up-to-date @@ -106,6 +144,11 @@ Singleton { } checkAirplaneMode.running = true; } + function onEnabledChanged() { + if (adapter && adapter.enabled && root.autoConnectEnabled) { + autoConnectTimer.restart(); + } + } } onAdapterChanged: { @@ -597,6 +640,33 @@ Singleton { forgetDevice(device); } + function attemptAutoConnect() { + if (_autoConnectInProgress) return; + if (airplaneModeEnabled) return; + if (!adapter || !adapter.enabled) return; + if (!autoConnectEnabled) return; + + _autoConnectInProgress = true; + + var devList = adapter.devices.values.filter(function(dev) { + return dev && dev.paired && !dev.connected && !dev.blocked; + }); + + var count = devList.length; + for (var i = 0; i < devList.length; i++) { + Logger.i("Bluetooth", "Auto-connecting to:", devList[i].name || devList[i].deviceName); + connectDeviceWithTrust(devList[i]); + } + + if (count > 0) { + ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("toast.bluetooth.auto-connecting", { + count: count + }), "bluetooth"); + } + + _autoConnectInProgress = false; + } + function connectDeviceWithTrust(device) { if (!device) { return; From fca7e360ab2d8c614046c4043db041dc9b5ff486 Mon Sep 17 00:00:00 2001 From: cbxcvl Date: Thu, 26 Feb 2026 14:49:59 -0300 Subject: [PATCH 2/3] fix(bluetooth): remove redundant auto-trust Instantiator The pairing script already calls `bluetoothctl trust` after a successful pair, so the `!modelData.trusted` condition was never true. --- Services/Networking/BluetoothService.qml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/Services/Networking/BluetoothService.qml b/Services/Networking/BluetoothService.qml index f97f8d0cb..1e98105d0 100644 --- a/Services/Networking/BluetoothService.qml +++ b/Services/Networking/BluetoothService.qml @@ -93,22 +93,6 @@ Singleton { onTriggered: root.attemptAutoConnect() } - Instantiator { - id: deviceWatcher - active: root.autoConnectEnabled && root.adapter !== null - model: root.adapter ? root.adapter.devices : null - delegate: Connections { - required property var modelData - target: modelData - function onPairedChanged() { - if (modelData.paired && !modelData.trusted) { - Logger.i("Bluetooth", "Auto-trusting newly paired device:", modelData.name || modelData.deviceName); - modelData.trusted = true; - } - } - } - } - function init() { Logger.i("Bluetooth", "Service started"); } From cfaf900f3f7fe5975b6adbe6cb9cafca7497f3da Mon Sep 17 00:00:00 2001 From: cbxcvl Date: Tue, 3 Mar 2026 16:19:52 -0300 Subject: [PATCH 3/3] refactor(bluetooth): address code review feedback - Replace mirrored autoConnectEnabled property with Connections block - Remove no-op _autoConnectInProgress flag (device.connect() is async) - Remove redundant count variable, use devList.length directly --- Services/Networking/BluetoothService.qml | 32 ++++++++++-------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/Services/Networking/BluetoothService.qml b/Services/Networking/BluetoothService.qml index 1e98105d0..36415c0b1 100644 --- a/Services/Networking/BluetoothService.qml +++ b/Services/Networking/BluetoothService.qml @@ -67,15 +67,15 @@ Singleton { // Internal: temporarily pause discovery during pair/connect to reduce HCI churn property bool _discoveryWasRunning: false property bool _ctlInit: false - property bool _autoConnectInProgress: false - // Mirror the setting so we can react to changes via onAutoConnectEnabledChanged - property bool autoConnectEnabled: Settings?.data?.network?.bluetoothAutoConnect ?? true - onAutoConnectEnabledChanged: { - if (autoConnectEnabled && adapter && adapter.enabled) { - autoConnectTimer.restart(); - } else { - autoConnectTimer.stop(); + Connections { + target: Settings.data.network + function onBluetoothAutoConnectChanged() { + if ((Settings?.data?.network?.bluetoothAutoConnect ?? true) && adapter && adapter.enabled) { + autoConnectTimer.restart(); + } else { + autoConnectTimer.stop(); + } } } @@ -105,7 +105,7 @@ Singleton { Quickshell.execDetached(["rfkill", "block", "bluetooth"]); } // Auto-connect on startup if BT is already enabled - if (root.autoConnectEnabled && adapter && adapter.enabled) { + if ((Settings?.data?.network?.bluetoothAutoConnect ?? true) && adapter && adapter.enabled) { autoConnectTimer.restart(); } } @@ -129,7 +129,7 @@ Singleton { checkAirplaneMode.running = true; } function onEnabledChanged() { - if (adapter && adapter.enabled && root.autoConnectEnabled) { + if (adapter && adapter.enabled && (Settings?.data?.network?.bluetoothAutoConnect ?? true)) { autoConnectTimer.restart(); } } @@ -625,30 +625,24 @@ Singleton { } function attemptAutoConnect() { - if (_autoConnectInProgress) return; if (airplaneModeEnabled) return; if (!adapter || !adapter.enabled) return; - if (!autoConnectEnabled) return; - - _autoConnectInProgress = true; + if (!(Settings?.data?.network?.bluetoothAutoConnect ?? true)) return; var devList = adapter.devices.values.filter(function(dev) { return dev && dev.paired && !dev.connected && !dev.blocked; }); - var count = devList.length; for (var i = 0; i < devList.length; i++) { Logger.i("Bluetooth", "Auto-connecting to:", devList[i].name || devList[i].deviceName); connectDeviceWithTrust(devList[i]); } - if (count > 0) { + if (devList.length > 0) { ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("toast.bluetooth.auto-connecting", { - count: count + count: devList.length }), "bluetooth"); } - - _autoConnectInProgress = false; } function connectDeviceWithTrust(device) {