// Copyright (C) 2026 Danilo M. // // 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 Pairing pairing: Pairing {} 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 // Battery for devices BlueZ does not publish through org.bluez.Battery1. // Gamepads are the case: the Sony pads expose their charge as a kernel // power_supply rather than a BlueZ interface, so BluetoothDevice.battery // reads nothing for them. The DualSense driver names the supply // "ps-controller-battery-"; the parser takes any supply whose name // ends in a MAC, so the separator ("-" here, "_" under hid-sony) and the // driver do not matter. This map is keyed by lowercase MAC and holds 0-100. property var batteryPct: ({}) function batteryFor(address) { if (!address) return null; const v = mod.batteryPct[address.toLowerCase()]; return v === undefined ? null : v; } function refreshBatteries() { batProc.running = false; batProc.running = true; } // Reads every power_supply once. A supply earns a line only when its name // ends in "xx:xx:xx:xx:xx:xx", which excludes the system battery (BAT0) // and AC adapter while catching the controller batteries. The MAC is the // trailing 17 characters; the separator before it is not assumed. property Process batProc: Process { command: ["sh", "-c", "for d in /sys/class/power_supply/*; do n=${d##*/}; " + "case $n in *??:??:??:??:??:??) " + "[ -r \"$d/capacity\" ] && printf '%s %s\\n' \"$n\" \"$(cat \"$d/capacity\")\";; " + "esac; done"] stdout: StdioCollector { onStreamFinished: { const map = {}; for (const line of text.trim().split("\n")) { const i = line.lastIndexOf(" "); if (i <= 0) continue; const pct = parseInt(line.slice(i + 1), 10); if (!isNaN(pct)) map[line.slice(0, i).slice(-17).toLowerCase()] = pct; } mod.batteryPct = map; } } } // ponytail: 30s poll, matches how slowly a gamepad charge moves. Move to a // uevent watch only if a lagging percentage is ever noticed. property Timer batTimer: Timer { interval: 30000 repeat: true running: mod.anyConnected onTriggered: mod.refreshBatteries() } onAnyConnectedChanged: if (mod.anyConnected) mod.refreshBatteries() Component.onCompleted: mod.refreshBatteries() // Null for the first ~2s, then filled. Safe navigation everywhere. icon: mod.adapter && mod.adapter.enabled ? "\uf293" : "\uf05e" // Active while anything is connected. active: mod.anyConnected 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 {} // Pairing runs through bluetoothctl; on success connect natively, on // failure notify, since the drawer may have closed by then. property Connections pairingConn: Connections { target: mod.pairing function onFinished(address, ok) { if (!ok) { mod.notify("Bluetooth", "Pairing failed: " + mod.pairing.error); return; } const d = mod.devices.find(x => x.address === address); if (d) d.connect(); } } tileContent: Component { BluetoothTile { bt: mod } } page: Component { Page { title: "Bluetooth" BluetoothPage { width: parent.width; bt: mod } } } }