diff options
| -rw-r--r-- | desktop/modules/bluetooth/BluetoothModule.qml | 55 | ||||
| -rw-r--r-- | desktop/modules/bluetooth/BluetoothPage.qml | 127 | ||||
| -rw-r--r-- | desktop/modules/bluetooth/BluetoothRow.qml | 85 | ||||
| -rw-r--r-- | desktop/modules/bluetooth/BluetoothTile.qml | 26 | ||||
| -rw-r--r-- | desktop/shell.qml | 2 |
5 files changed, 295 insertions, 0 deletions
diff --git a/desktop/modules/bluetooth/BluetoothModule.qml b/desktop/modules/bluetooth/BluetoothModule.qml new file mode 100644 index 0000000..84ac9a4 --- /dev/null +++ b/desktop/modules/bluetooth/BluetoothModule.qml @@ -0,0 +1,55 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +import Quickshell +import Quickshell.Bluetooth +import Quickshell.Io +import QtQuick +import "../.." + +// Always active: the BlueZ backend pushes and there is no poll to gate. +// Referencing Bluetooth here instantiates it at shell start. +Module { + id: mod + + name: "bluetooth" + label: "Bluetooth" + alwaysActive: true + + readonly property var adapter: Bluetooth.defaultAdapter + readonly property var devices: Bluetooth.devices ? Bluetooth.devices.values : [] + readonly property var connectedDevices: devices.filter(d => d.connected) + readonly property var pairedDevices: devices.filter(d => d.paired && !d.connected) + readonly property var availableDevices: devices.filter(d => !d.paired) + + readonly property bool anyConnected: connectedDevices.length > 0 + + // Null for the first ~2s, then filled. Safe navigation everywhere. + icon: mod.adapter && mod.adapter.enabled ? "\uf293" : "\uf05e" + + function notify(title, body) { + notifyProc.command = ["notify-send", "--app-name=bluetooth", + "--urgency=critical", "--icon=error", title, body]; + notifyProc.running = false; + notifyProc.running = true; + } + + property Process notifyProc: Process {} + + tileContent: Component { BluetoothTile { bt: mod } } + + page: Component { + Page { + title: "Bluetooth" + BluetoothPage { width: parent.width; bt: mod } + } + } +} diff --git a/desktop/modules/bluetooth/BluetoothPage.qml b/desktop/modules/bluetooth/BluetoothPage.qml new file mode 100644 index 0000000..c608d82 --- /dev/null +++ b/desktop/modules/bluetooth/BluetoothPage.qml @@ -0,0 +1,127 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +import QtQuick +import QtQuick.Controls +import "../.." + +Column { + id: page + + required property var bt + signal back + + spacing: 14 + + // --- Adapter --- + + Item { + width: page.width + implicitHeight: Math.max(btLabel.implicitHeight, powerSwitch.implicitHeight) + + Text { + id: btLabel + anchors.verticalCenter: parent.verticalCenter + text: "Bluetooth" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 1; bold: true } + color: Theme.text + } + + Switch { + id: powerSwitch + anchors { right: parent.right; verticalCenter: parent.verticalCenter } + checked: page.bt.adapter ? page.bt.adapter.enabled : false + onToggled: if (page.bt.adapter) page.bt.adapter.enabled = checked + } + } + + Text { + width: page.width + visible: !(page.bt.adapter && page.bt.adapter.enabled) + text: "Bluetooth off" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } + color: Theme.subtext + } + + Item { + width: page.width + visible: page.bt.adapter && page.bt.adapter.enabled + implicitHeight: Math.max(visLabel.implicitHeight, visSwitch.implicitHeight) + + Text { + id: visLabel + anchors.verticalCenter: parent.verticalCenter + text: "Visible" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } + color: Theme.text + } + + Switch { + id: visSwitch + anchors { right: parent.right; verticalCenter: parent.verticalCenter } + checked: page.bt.adapter ? page.bt.adapter.discoverable : false + onToggled: if (page.bt.adapter) page.bt.adapter.discoverable = checked + } + } + + Row { + width: page.width + visible: page.bt.adapter && page.bt.adapter.enabled + spacing: 8 + + Button { + text: page.bt.adapter && page.bt.adapter.discovering ? "Stop scan" : "Scan" + onClicked: if (page.bt.adapter) page.bt.adapter.discovering = !page.bt.adapter.discovering + } + } + + // --- Connected --- + + Text { + width: page.width + visible: page.bt.connectedDevices.length > 0 + text: "Connected" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true } + color: Theme.subtext + } + + Repeater { + model: page.bt.connectedDevices + + BluetoothRow { + required property var modelData + bt: page.bt + device: modelData + width: page.width + } + } + + // --- Paired --- + + Text { + width: page.width + visible: page.bt.pairedDevices.length > 0 + text: "Paired" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true } + color: Theme.subtext + } + + Repeater { + model: page.bt.pairedDevices + + BluetoothRow { + required property var modelData + bt: page.bt + device: modelData + width: page.width + } + } +} diff --git a/desktop/modules/bluetooth/BluetoothRow.qml b/desktop/modules/bluetooth/BluetoothRow.qml new file mode 100644 index 0000000..d92bd9a --- /dev/null +++ b/desktop/modules/bluetooth/BluetoothRow.qml @@ -0,0 +1,85 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +import QtQuick +import "../.." + +// One tracked device. The actions shown depend on its state: connected, +// paired-but-idle, or found and not yet paired. +Rectangle { + id: row + + required property var bt + required property var device + + implicitHeight: col.implicitHeight + 16 + radius: 8 + color: device.connected ? Qt.alpha(Theme.accent, 0.10) : Qt.alpha(Theme.surface, 0.35) + + Column { + id: col + anchors { left: parent.left; right: parent.right; top: parent.top; margins: 10 } + spacing: 8 + + Item { + width: parent.width + implicitHeight: Math.max(nameText.implicitHeight, actions.implicitHeight) + + Text { + id: nameText + anchors { left: parent.left; right: actions.left; rightMargin: 8; verticalCenter: parent.verticalCenter } + elide: Text.ElideRight + text: row.device.name || row.device.deviceName || row.device.address + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: row.device.connected } + color: row.device.connected ? Theme.accent : Theme.text + } + + Row { + id: actions + anchors { right: parent.right; verticalCenter: parent.verticalCenter } + spacing: 6 + + Text { + anchors.verticalCenter: parent.verticalCenter + visible: row.device.batteryAvailable ?? false + text: Math.round(row.device.battery ?? 0) + "%" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: Theme.overlay + } + + Button { + visible: !row.device.connected && row.device.paired + text: "Connect" + onClicked: row.device.connect() + } + + Button { + visible: row.device.connected + text: "Disconnect" + onClicked: row.device.disconnect() + } + + Button { + visible: row.device.paired + text: row.device.trusted ? "Untrust" : "Trust" + onClicked: row.device.trusted = !row.device.trusted + } + + Button { + visible: row.device.paired + text: "Forget" + danger: true + onClicked: row.device.forget() + } + } + } + } +} diff --git a/desktop/modules/bluetooth/BluetoothTile.qml b/desktop/modules/bluetooth/BluetoothTile.qml new file mode 100644 index 0000000..1489f16 --- /dev/null +++ b/desktop/modules/bluetooth/BluetoothTile.qml @@ -0,0 +1,26 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +import QtQuick +import "../.." + +Text { + required property var bt + + width: parent ? parent.width : implicitWidth + elide: Text.ElideRight + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: bt.anyConnected ? Theme.text : Theme.subtext + text: !bt.adapter || !bt.adapter.enabled ? "Off" + : bt.connectedDevices.length === 0 ? "No devices" + : bt.connectedDevices.length === 1 ? bt.connectedDevices[0].name + : bt.connectedDevices.length + " connected" +} diff --git a/desktop/shell.qml b/desktop/shell.qml index 5f9640d..d191983 100644 --- a/desktop/shell.qml +++ b/desktop/shell.qml @@ -13,6 +13,7 @@ import Quickshell import Quickshell.Io import Quickshell.Wayland import "modules/appearance" +import "modules/bluetooth" import "modules/mail" import "modules/network" import "modules/sound" @@ -36,6 +37,7 @@ ShellRoot { modules: [ SoundModule {}, NetworkModule {}, + BluetoothModule {}, MailModule {}, AppearanceModule {}, VmModule {}, |
