From 61b21f389f90c2d6c7ed9cd1b206b6a31ae93d86 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 21 Sep 2025 17:33:28 +1000 Subject: feat: add battery warnings Closes #73 Closes #117 --- README.md | 27 ++++++++++++++++- config/GeneralConfig.qml | 26 ++++++++++++++++ modules/BatteryMonitor.qml | 54 ++++++++++++++++++++++++++++++++++ modules/utilities/toasts/ToastItem.qml | 3 ++ modules/utilities/toasts/Toasts.qml | 7 +++++ shell.qml | 1 + 6 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 modules/BatteryMonitor.qml diff --git a/README.md b/README.md index c63769d..421b012 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,30 @@ default, you must create it manually. "terminal": ["foot"], "audio": ["pavucontrol"] }, + "battery": { + "warnLevels": [ + { + "level": 20, + "title": "Low battery", + "message": "You might want to plug in a charger", + "icon": "battery_android_frame_2" + }, + { + "level": 10, + "title": "Did you see the previous message?", + "message": "You should probably plug in a charger now", + "icon": "battery_android_frame_1" + }, + { + "level": 5, + "title": "Critical battery level", + "message": "PLUG THE CHARGER RIGHT NOW!!", + "icon": "battery_android_alert", + "critical": true + } + ], + "criticalLevel": 3 + }, "idle": { "inhibitWhenAudio": true, "lockTimeout": 180, @@ -530,7 +554,8 @@ default, you must create it manually. "enabled": true }, "utilities": { - "enabled": true + "enabled": true, + "maxToasts": 4 } } ``` diff --git a/config/GeneralConfig.qml b/config/GeneralConfig.qml index dc6222e..4d7a79c 100644 --- a/config/GeneralConfig.qml +++ b/config/GeneralConfig.qml @@ -3,6 +3,7 @@ import Quickshell.Io JsonObject { property Apps apps: Apps {} property Idle idle: Idle {} + property Battery battery: Battery {} component Apps: JsonObject { property list terminal: ["foot"] @@ -17,4 +18,29 @@ JsonObject { property real dpmsTimeout: 300 // 5 mins property real sleepTimeout: 600 // 10 mins } + + component Battery: JsonObject { + property list warnLevels: [ + { + level: 20, + title: qsTr("Low battery"), + message: qsTr("You might want to plug in a charger"), + icon: "battery_android_frame_2" + }, + { + level: 10, + title: qsTr("Did you see the previous message?"), + message: qsTr("You should probably plug in a charger now"), + icon: "battery_android_frame_1" + }, + { + level: 5, + title: qsTr("Critical battery level"), + message: qsTr("PLUG THE CHARGER RIGHT NOW!!"), + icon: "battery_android_alert", + critical: true + }, + ] + property int criticalLevel: 3 + } } diff --git a/modules/BatteryMonitor.qml b/modules/BatteryMonitor.qml new file mode 100644 index 0000000..f407edf --- /dev/null +++ b/modules/BatteryMonitor.qml @@ -0,0 +1,54 @@ +import qs.config +import Caelestia +import Quickshell +import Quickshell.Services.UPower +import QtQuick + +Scope { + id: root + + readonly property list warnLevels: [...Config.general.battery.warnLevels].sort((a, b) => b.level - a.level) + + Connections { + target: UPower + + function onOnBatteryChanged(): void { + if (UPower.onBattery) { + Toaster.toast(qsTr("Charger unplugged"), qsTr("Battery is now on AC"), "power_off"); + } else { + Toaster.toast(qsTr("Charger plugged in"), qsTr("Battery is charging"), "battery_android_frame_bolt"); + for (const level of root.warnLevels) + level.warned = false; + } + } + } + + Connections { + target: UPower.displayDevice + + function onPercentageChanged(): void { + if (!UPower.onBattery) + return; + + const p = UPower.displayDevice.percentage; + for (const level of root.warnLevels) { + if (p <= level.level && !level.warned) { + level.warned = true; + Toaster.toast(level.title ?? qsTr("Battery warning"), level.message ?? qsTr("Battery level is low"), level.icon ?? "battery_android_alert", level.critical ? Toast.Error : Toast.Warning); + } + } + + if (!hibernateTimer.running && p <= Config.general.battery.criticalLevel) { + Toaster.toast(qsTr("Hibernating in 5 seconds"), qsTr("Hibernating to prevent data loss"), "battery_android_alert", Toast.Error); + hibernateTimer.start(); + } + } + } + + Timer { + id: hibernateTimer + + interval: 5000 + onTriggered: Quickshell.execDetached(["systemctl", "hibernate"]) + } +} diff --git a/modules/utilities/toasts/ToastItem.qml b/modules/utilities/toasts/ToastItem.qml index fa3aa18..481b831 100644 --- a/modules/utilities/toasts/ToastItem.qml +++ b/modules/utilities/toasts/ToastItem.qml @@ -107,10 +107,12 @@ StyledRect { return Colours.palette.m3onSurface; } font.pointSize: Appearance.font.size.normal + elide: Text.ElideRight } StyledText { Layout.fillWidth: true + textFormat: Text.StyledText text: root.modelData.message color: { if (root.modelData.type === Toast.Success) @@ -122,6 +124,7 @@ StyledRect { return Colours.palette.m3onSurface; } opacity: 0.8 + elide: Text.ElideRight } } } diff --git a/modules/utilities/toasts/Toasts.qml b/modules/utilities/toasts/Toasts.qml index c23790d..c9a8d4d 100644 --- a/modules/utilities/toasts/Toasts.qml +++ b/modules/utilities/toasts/Toasts.qml @@ -58,6 +58,11 @@ Item { return index >= Config.utilities.maxToasts + extraHidden; } + onPreviewHiddenChanged: { + if (initAnim.running && previewHidden) + initAnim.stop(); + } + opacity: modelData.closed || previewHidden ? 0 : 1 scale: modelData.closed || previewHidden ? 0.7 : 1 @@ -83,6 +88,8 @@ Item { Component.onCompleted: modelData.lock(this) Anim { + id: initAnim + Component.onCompleted: running = !toast.previewHidden target: toast diff --git a/shell.qml b/shell.qml index a56caae..3ce7776 100644 --- a/shell.qml +++ b/shell.qml @@ -18,6 +18,7 @@ ShellRoot { } Shortcuts {} + BatteryMonitor {} IdleMonitors { lock: lock } -- cgit v1.2.3-freya