Personal noctalia plugins collection

Add battery charge limit toggle widget

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+218
+109
battery/BarWidget.qml
··· 1 + import QtQuick 2 + import Quickshell 3 + import Quickshell.Services.UPower 4 + import qs.Commons 5 + import qs.Widgets 6 + import qs.Modules.Bar.Extras 7 + import qs.Services.Hardware 8 + import qs.Services.UI 9 + 10 + Item { 11 + id: root 12 + 13 + property QtObject pluginApi: null 14 + readonly property QtObject pluginCore: pluginApi?.mainInstance 15 + 16 + property ShellScreen screen 17 + property string widgetId: "" 18 + property string section: "" 19 + property int sectionWidgetIndex: -1 20 + property int sectionWidgetsCount: 0 21 + 22 + // Battery state from BatteryService 23 + readonly property var selectedDevice: BatteryService.displayDevice 24 + readonly property bool isReady: BatteryService.isDeviceReady(selectedDevice) 25 + readonly property real percent: isReady ? BatteryService.getPercentage(selectedDevice) : -1 26 + readonly property bool isCharging: isReady ? BatteryService.isCharging(selectedDevice) : false 27 + readonly property bool isPluggedIn: isReady ? BatteryService.isPluggedIn(selectedDevice) : false 28 + readonly property bool isLowBattery: isReady ? BatteryService.isLowBattery(selectedDevice) : false 29 + readonly property bool isCriticalBattery: isReady ? BatteryService.isCriticalBattery(selectedDevice) : false 30 + 31 + readonly property var tooltipContent: { 32 + let rows = []; 33 + if (isReady) { 34 + rows.push(["Battery", percent + "%"]); 35 + 36 + let timeText = BatteryService.getTimeRemainingText(selectedDevice); 37 + if (timeText) { 38 + const idx = timeText.indexOf(":"); 39 + if (idx >= 0) 40 + rows.push([timeText.substring(0, idx).trim(), timeText.substring(idx + 1).trim()]); 41 + else 42 + rows.push([timeText, ""]); 43 + } 44 + 45 + let rateText = BatteryService.getRateText(selectedDevice); 46 + if (rateText) { 47 + const idx = rateText.indexOf(":"); 48 + if (idx >= 0) 49 + rows.push([rateText.substring(0, idx).trim(), rateText.substring(idx + 1).trim()]); 50 + else 51 + rows.push([rateText, ""]); 52 + } 53 + } 54 + 55 + if (pluginCore) { 56 + if (rows.length > 0) rows.push(["---", "---"]); 57 + rows.push(["Charge limit", pluginCore.chargeLimit + "%"]); 58 + } 59 + 60 + return rows; 61 + } 62 + 63 + implicitWidth: pill.width 64 + implicitHeight: pill.height 65 + 66 + BarPill { 67 + id: pill 68 + screen: root.screen 69 + oppositeDirection: BarService.getPillDirection(root) 70 + forceClose: false 71 + 72 + icon: BatteryService.getIcon(root.percent, root.isCharging, root.isPluggedIn, root.isReady) 73 + text: root.isReady ? root.percent : "-" 74 + suffix: pluginCore && pluginCore.chargeLimit < 100 ? "% \u2193" + pluginCore.chargeLimit : "%" 75 + autoHide: false 76 + customBackgroundColor: root.isCharging ? Color.mPrimary : ((root.isLowBattery || root.isCriticalBattery) ? Color.mError : "transparent") 77 + customTextIconColor: root.isCharging ? Color.mOnPrimary : ((root.isLowBattery || root.isCriticalBattery) ? Color.mOnError : "transparent") 78 + tooltipText: root.tooltipContent 79 + 80 + onClicked: pluginCore?.toggle() 81 + 82 + onRightClicked: { 83 + PanelService.showContextMenu(contextMenu, pill, root.screen); 84 + } 85 + } 86 + 87 + NPopupContextMenu { 88 + id: contextMenu 89 + model: [ 90 + { 91 + "label": pluginCore && pluginCore.chargeLimit < 100 92 + ? "Set charge limit to 100%" 93 + : "Set charge limit to 80%", 94 + "action": "toggle", 95 + "icon": "battery_saver", 96 + "enabled": true 97 + } 98 + ] 99 + 100 + onTriggered: action => { 101 + contextMenu.close(); 102 + PanelService.closeContextMenu(root.screen); 103 + 104 + if (action === "toggle") { 105 + pluginCore?.toggle(); 106 + } 107 + } 108 + } 109 + }
+80
battery/Main.qml
··· 1 + import QtQuick 2 + import Quickshell 3 + import Quickshell.Io 4 + 5 + Item { 6 + id: root 7 + property var pluginApi: null 8 + 9 + property int chargeLimit: 100 10 + property bool reading: false 11 + 12 + readonly property string sysPath: "/sys/class/power_supply/BAT1/charge_control_end_threshold" 13 + 14 + Component.onCompleted: readLimit() 15 + 16 + Timer { 17 + interval: 5000 18 + running: true 19 + repeat: true 20 + onTriggered: readLimit() 21 + } 22 + 23 + function readLimit() { 24 + if (reading) return 25 + reading = true 26 + readProcess.running = true 27 + } 28 + 29 + function toggle() { 30 + var newVal = (chargeLimit >= 100) ? 80 : 100 31 + writeStdout = "" 32 + writeStderr = "" 33 + writeProcess.command = ["pkexec", "tee", sysPath] 34 + writeProcess.running = true 35 + writeProcess.write(newVal + "\n") 36 + writeProcess.closeWriteChannel() 37 + } 38 + 39 + // --- read current value --- 40 + property string readStdout: "" 41 + 42 + Process { 43 + id: readProcess 44 + command: ["cat", root.sysPath] 45 + onExited: function(exitCode, exitStatus) { 46 + if (exitCode === 0) { 47 + var val = parseInt(readStdout.trim(), 10) 48 + if (!isNaN(val)) root.chargeLimit = val 49 + } 50 + readStdout = "" 51 + root.reading = false 52 + } 53 + stdout: SplitParser { 54 + onRead: data => root.readStdout += data 55 + } 56 + } 57 + 58 + // --- write new value --- 59 + property string writeStdout: "" 60 + property string writeStderr: "" 61 + 62 + Process { 63 + id: writeProcess 64 + onExited: function(exitCode, exitStatus) { 65 + if (exitCode === 0) { 66 + Qt.callLater(readLimit) 67 + } else { 68 + console.error("[battery-limit] pkexec tee failed: " + writeStderr) 69 + } 70 + writeStdout = "" 71 + writeStderr = "" 72 + } 73 + stdout: SplitParser { 74 + onRead: data => root.writeStdout += data 75 + } 76 + stderr: SplitParser { 77 + onRead: data => root.writeStderr += data 78 + } 79 + } 80 + }
+17
battery/manifest.json
··· 1 + { 2 + "id": "battery", 3 + "name": "Battery", 4 + "version": "1.0.0", 5 + "minNoctaliaVersion": "4.2.3", 6 + "author": "dodaars", 7 + "license": "MIT", 8 + "description": "Toggle battery charge limit between 80% and 100% to extend battery lifespan.", 9 + "tags": ["Bar", "System"], 10 + "entryPoints": { 11 + "main": "Main.qml", 12 + "barWidget": "BarWidget.qml" 13 + }, 14 + "dependencies": { 15 + "plugins": [] 16 + } 17 + }
+12
registry.json
··· 13 13 "license": "MIT", 14 14 "tags": ["Bar", "Panel", "Productivity"], 15 15 "lastUpdated": "2026-02-28T12:00:00+08:00" 16 + }, 17 + { 18 + "id": "battery", 19 + "name": "Battery", 20 + "version": "1.0.0", 21 + "official": false, 22 + "author": "dodaars", 23 + "description": "Toggle battery charge limit between 80% and 100% to extend battery lifespan.", 24 + "minNoctaliaVersion": "4.2.3", 25 + "license": "MIT", 26 + "tags": ["Bar", "System"], 27 + "lastUpdated": "2026-03-10T12:00:00+08:00" 16 28 } 17 29 ] 18 30 }