mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
c7c147e1ce
This reverts commit a14d80abdd.
402 lines
11 KiB
QML
402 lines
11 KiB
QML
pragma Singleton
|
|
import QtQuick
|
|
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.Services.UPower
|
|
import qs.Commons
|
|
import qs.Services.Networking
|
|
import qs.Services.UI
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
// MARK: BatteryService
|
|
// Primary battery device (prioritizes laptop over Bluetooth)
|
|
readonly property var primaryDevice: _laptopBattery || _bluetoothBattery || null
|
|
// Whether the primary device is a laptop battery
|
|
readonly property bool isLaptopBattery: _laptopBattery !== null && primaryDevice === _laptopBattery
|
|
readonly property real batteryPercentage: getPercentage(primaryDevice)
|
|
readonly property bool batteryCharging: isCharging(primaryDevice)
|
|
readonly property bool batteryPluggedIn: isPluggedIn(primaryDevice)
|
|
readonly property bool batteryReady: isDeviceReady(primaryDevice)
|
|
readonly property bool batteryPresent: isDevicePresent(primaryDevice)
|
|
|
|
property bool healthAvailable: false
|
|
property int healthPercent: -1
|
|
|
|
readonly property var _laptopBattery: {
|
|
if (!UPower.devices)
|
|
return UPower.displayDevice;
|
|
|
|
// 2. Determine the primary device (System Battery)
|
|
readonly property var primaryDevice: {
|
|
if (devices.length === 0)
|
|
return null;
|
|
|
|
// Prioritize DisplayDevice (Aggregate)
|
|
if (UPower.displayDevice && UPower.displayDevice.type === UPowerDeviceType.Battery && isDevicePresent(UPower.displayDevice)) {
|
|
return UPower.displayDevice;
|
|
}
|
|
|
|
// Prioritize BAT0
|
|
for (var i = 0; i < devices.length; i++) {
|
|
var d = devices[i];
|
|
if (d && (d.nativePath === "BAT0" || d.objectPath === "/org/freedesktop/UPower/devices/battery_BAT0")) {
|
|
return d;
|
|
}
|
|
}
|
|
|
|
// Prioritize (any) Laptop Battery
|
|
for (var j = 0; j < devices.length; j++) {
|
|
var dev = devices[j];
|
|
if (dev && !isBluetoothDevice(dev) && dev.isLaptopBattery) {
|
|
return dev;
|
|
}
|
|
}
|
|
|
|
if (UPower.displayDevice.isPresent) {
|
|
return UPower.displayDevice;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
readonly property var _bluetoothBattery: {
|
|
if (externalBatteries.length > 0)
|
|
return externalBatteries[0];
|
|
return null;
|
|
}
|
|
|
|
// MARK: resolveDevice
|
|
function resolveDevice(nativePath) {
|
|
if (!nativePath || nativePath === "") {
|
|
return primaryDevice;
|
|
}
|
|
|
|
// Check for DisplayDevice explicitly (Literal key OR actual native path)
|
|
if ((nativePath === "DisplayDevice" || (UPower.displayDevice && nativePath === UPower.displayDevice.nativePath)) && UPower.displayDevice) {
|
|
return UPower.displayDevice;
|
|
}
|
|
|
|
var upowerDev = findUPowerDevice(nativePath);
|
|
if (upowerDev)
|
|
return upowerDev;
|
|
|
|
var btDev = findBluetoothDevice(nativePath);
|
|
if (btDev)
|
|
return btDev;
|
|
|
|
return null;
|
|
}
|
|
|
|
// MARK: findUPowerDevice
|
|
function findUPowerDevice(nativePath) {
|
|
if (!nativePath || nativePath === "" || nativePath === "DisplayDevice") {
|
|
return _laptopBattery;
|
|
}
|
|
|
|
Timer {
|
|
interval: 500
|
|
running: true
|
|
repeat: false
|
|
onTriggered: root.initializationComplete = true
|
|
}
|
|
|
|
// MARK: findBluetoothDevice
|
|
function findBluetoothDevice(nativePath) {
|
|
if (!nativePath || !BluetoothService.devices) {
|
|
return null;
|
|
}
|
|
|
|
// Check for DisplayDevice explicitly if requested via "DisplayDevice" or empty string
|
|
if (nativePath === "DisplayDevice" && UPower.displayDevice) {
|
|
return UPower.displayDevice;
|
|
}
|
|
|
|
// Search in our cached list
|
|
for (var i = 0; i < devices.length; i++) {
|
|
var d = devices[i];
|
|
if (isBluetoothDevice(d)) {
|
|
if (d.address && d.address.toUpperCase() === nativePath.toUpperCase())
|
|
return d;
|
|
// Try matching MAC in path string if passed format differs
|
|
if (nativePath.includes(d.address.toUpperCase()))
|
|
return d;
|
|
} else {
|
|
if (d.nativePath === nativePath)
|
|
return d;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// MARK: isDevicePresent
|
|
function isDevicePresent(device) {
|
|
if (!device)
|
|
return false;
|
|
|
|
// Handle Bluetooth devices
|
|
if (device.batteryAvailable !== undefined) {
|
|
return device.connected === true;
|
|
}
|
|
|
|
// Handle UPower devices
|
|
if (device.type !== undefined) {
|
|
if (device.type === UPowerDeviceType.Battery && device.isPresent !== undefined) {
|
|
return device.isPresent === true;
|
|
}
|
|
return device.ready && device.percentage !== undefined;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// MARK: isDeviceReady
|
|
function isDeviceReady(device) {
|
|
if (!isDevicePresent(device))
|
|
return false;
|
|
|
|
if (device.batteryAvailable !== undefined) {
|
|
return device.battery !== undefined;
|
|
}
|
|
return device.ready && device.percentage !== undefined;
|
|
}
|
|
|
|
// MARK: getPercentage
|
|
function getPercentage(device) {
|
|
if (!device)
|
|
return 0;
|
|
if (device.batteryAvailable !== undefined) {
|
|
return (device.battery || 0) * 100;
|
|
}
|
|
return (device.percentage || 0) * 100;
|
|
}
|
|
|
|
// MARK: isCharging
|
|
function isCharging(device) {
|
|
if (!device || isBluetoothDevice(device))
|
|
// Tracking bluetooth devices can charge or not is a loop hole, none of my devices has it, even if it possible?!
|
|
return false; // Assuming not charging until someone/quickshell brings a way to do pretty unlikely.
|
|
if (device.state !== undefined) {
|
|
return device.state === UPowerDeviceState.Charging;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// MARK: isPluggedIn
|
|
function isPluggedIn(device) {
|
|
if (!device || isBluetoothDevice(device))
|
|
// Tracking bluetooth devices can charge or not is a loop hole, none of my devices has it, even if it possible?!
|
|
return false; // Assuming not charging until someone/quickshell brings a way to do pretty unlikely.
|
|
if (device.state !== undefined) {
|
|
return device.state === UPowerDeviceState.FullyCharged || device.state === UPowerDeviceState.PendingCharge;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// MARK: isBluetoothDevice
|
|
function isBluetoothDevice(device) {
|
|
return device && device.batteryAvailable !== undefined;
|
|
}
|
|
|
|
// MARK: getDeviceName
|
|
function getDeviceName(device) {
|
|
if (!isDeviceReady(device))
|
|
return "";
|
|
|
|
// Don't show name for laptop batteries
|
|
if (!isBluetoothDevice(device) && device.isLaptopBattery) {
|
|
return "";
|
|
}
|
|
|
|
if (isBluetoothDevice(device) && device.name) {
|
|
return device.name;
|
|
}
|
|
|
|
if (device.model) {
|
|
return device.model;
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
// MARK: refreshHealth
|
|
function refreshHealth() {
|
|
if (!isLaptopBattery) {
|
|
healthAvailable = false;
|
|
healthPercent = -1;
|
|
return;
|
|
}
|
|
healthProcess.running = true;
|
|
}
|
|
|
|
Process {
|
|
id: healthProcess
|
|
command: ["sh", "-c", `upower -i ${primaryDevice && primaryDevice.nativePath ? "/org/freedesktop/UPower/devices/battery_" + primaryDevice.nativePath : "$(upower -e | grep battery | head -n 1)"} 2>/dev/null | grep -iE 'capacity'`]
|
|
environment: ({
|
|
"LC_ALL": "C"
|
|
})
|
|
|
|
stdout: SplitParser {
|
|
onRead: function (data) {
|
|
var line = data.trim();
|
|
if (line === "")
|
|
return;
|
|
|
|
var capacityMatch = line.match(/^\s*capacity:\s*(\d+(?:\.\d+)?)\s*%/i);
|
|
if (capacityMatch) {
|
|
root.healthPercent = Math.round(parseFloat(capacityMatch[1]));
|
|
root.healthAvailable = true;
|
|
Logger.d("Battery", "Health retrieved from CLI:", root.healthPercent + "%");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
if (isLaptopBattery) {
|
|
Qt.callLater(refreshHealth);
|
|
}
|
|
}
|
|
// MARK: getIcon
|
|
function getIcon(percent, charging, pluggedIn, isReady) {
|
|
if (!isReady) {
|
|
return "battery-exclamation";
|
|
}
|
|
if (charging) {
|
|
return "battery-charging";
|
|
}
|
|
if (pluggedIn) {
|
|
return "battery-charging-2";
|
|
}
|
|
if (percent >= 80) {
|
|
return "battery-4";
|
|
}
|
|
if (percent >= 60) {
|
|
return "battery-3";
|
|
}
|
|
if (percent >= 40) {
|
|
return "battery-2";
|
|
}
|
|
if (percent >= 20) {
|
|
return "battery-1";
|
|
}
|
|
if (percent >= 0) {
|
|
return "battery";
|
|
}
|
|
return "battery-off"; // New fallback icon clearly represent if nothing is true here.
|
|
}
|
|
|
|
// MARK: hasAnyBattery
|
|
function hasAnyBattery() {
|
|
return primaryDevice !== null;
|
|
}
|
|
|
|
|
|
// MARK: Battery
|
|
// MARK: getRateText
|
|
function getRateText(device) {
|
|
if (!device || device.changeRate === undefined)
|
|
return "";
|
|
|
|
const rate = Math.abs(device.changeRate);
|
|
if (isPluggedIn(device)) {
|
|
return I18n.tr("battery.plugged-in");
|
|
} else if (isCharging(device)) {
|
|
return I18n.tr("battery.charging-rate", {
|
|
"rate": rate.toFixed(2)
|
|
});
|
|
} else {
|
|
return I18n.tr("battery.discharging-rate", {
|
|
"rate": rate.toFixed(2)
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
// MARK: BatteryPanel
|
|
readonly property var externalBatteries: {
|
|
var list = [];
|
|
var devices = BluetoothService.devices ? (BluetoothService.devices.values || []) : [];
|
|
for (var i = 0; i < devices.length; i++) {
|
|
var device = devices[i];
|
|
if (device && device.connected && device.batteryAvailable) {
|
|
list.push(device);
|
|
}
|
|
}
|
|
return list;
|
|
}
|
|
|
|
// MARK: getTimeRemainingText
|
|
function getTimeRemainingText(device) {
|
|
if (!isDeviceReady(device)) {
|
|
return I18n.tr("battery.no-battery-detected");
|
|
}
|
|
if (isPluggedIn(device)) {
|
|
return I18n.tr("battery.plugged-in");
|
|
}
|
|
if (device) {
|
|
if (device.timeToFull > 0) {
|
|
return I18n.tr("battery.time-until-full", {
|
|
"time": Time.formatVagueHumanReadableDuration(device.timeToFull)
|
|
});
|
|
}
|
|
if (device.timeToEmpty > 0) {
|
|
return I18n.tr("battery.time-left", {
|
|
"time": Time.formatVagueHumanReadableDuration(device.timeToEmpty)
|
|
});
|
|
}
|
|
}
|
|
return I18n.tr("common.idle");
|
|
}
|
|
|
|
|
|
// MARK: BatterySettings
|
|
property var devicesModel: buildDeviceModel()
|
|
|
|
function buildDeviceModel() {
|
|
var model = [
|
|
{
|
|
"key": UPower.devices.DisplayDevice || "", // It was capital D and i spend an hour to figure out [why tf this do absolutely nothing] XD (I hate my left shift it sticks)
|
|
"name": I18n.tr("bar.battery.device-default")
|
|
}
|
|
];
|
|
|
|
// UPower Devices
|
|
if (UPower.devices && UPower.devices.values) {
|
|
var deviceArray = UPower.devices.values;
|
|
for (var i = 0; i < deviceArray.length; i++) {
|
|
var device = deviceArray[i];
|
|
if (!device || device.type === UPowerDeviceType.LinePower) {
|
|
continue;
|
|
}
|
|
var displayName = device.model || device.nativePath || "Unknown";
|
|
model.push({
|
|
"key": device.nativePath || "",
|
|
"name": displayName
|
|
});
|
|
}
|
|
}
|
|
return model;
|
|
}
|
|
|
|
// MARK: modelUpdateTimer
|
|
Timer {
|
|
id: modelUpdateTimer
|
|
interval: 2000
|
|
running: true
|
|
repeat: true
|
|
onTriggered: {
|
|
var newModel = buildDeviceModel();
|
|
// Simple change detection to avoid unnecessary bindings updates
|
|
if (JSON.stringify(newModel) !== JSON.stringify(devicesModel)) {
|
|
devicesModel = newModel;
|
|
}
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: UPower.devices
|
|
function onValuesChanged() { modelUpdateTimer.restart(); devicesModel = buildDeviceModel(); }
|
|
}
|
|
}
|