diff --git a/battery-widget/README.md b/battery-widget/README.md index a3b56b4..da2e6f6 100644 --- a/battery-widget/README.md +++ b/battery-widget/README.md @@ -9,6 +9,10 @@ Desktop/lockscreen widget that displays the current battery level and charging s | ID | `yocraft/battery-widget` | | Entries | Desktop widget: `widget` | +## Requirements + +The plugin requires `gdbus` on `PATH` and `upower`. + ## Usage Add it to your desktop/lockscreen widgets using the widget editor. You can configure it in the widget settings. @@ -20,6 +24,7 @@ Add it to your desktop/lockscreen widgets using the widget editor. You can confi | `layout` | `select` | `horizontal` | Layout of the widget. | | `show_glyph` | `bool` | `true` | Show the battery icon. | | `show_label` | `bool` | `true` | Show text next to the icon. | +| `label_content` | `select` | `percent` | What the battery label shows: charge percentage, time remaining, or power draw. | | `hide_plugged` | `bool` | `false` | Hide the battery widget when connected to AC power. | | `hide_full` | `bool` | `false` | Hide the battery widget when fully charged. | | `color` | `color` | `on_surface` | Color role for this widget's icon and label; fixed hex colors are also supported. | @@ -27,7 +32,6 @@ Add it to your desktop/lockscreen widgets using the widget editor. You can confi | `charging_color` | `color` | `on_surface` | Color applied when connected to AC power. | | `battery` | `select` | `auto` | Detect the battery automatically or set a custom name. | | `battery_name` | `string` | `BAT0` | Set a custom name for the battery. | -| `refresh_interval` | `int` | `60` | Controls how frequently the widget updates its content. (in seconds) | ## Notes diff --git a/battery-widget/plugin.toml b/battery-widget/plugin.toml index c28a817..363bb11 100644 --- a/battery-widget/plugin.toml +++ b/battery-widget/plugin.toml @@ -1,14 +1,14 @@ id = "yocraft/battery-widget" name = "Battery Widget" -version = "1.0.1" -plugin_api = 19 +version = "2.0.0" +plugin_api = 3 author = "yocraft" license = "MIT" deprecated = false icon = "battery" description = "Desktop/Lockscreen widget to show battery." tags = [ "desktop", "hardware", "system", "utility" ] -dependencies = [] +dependencies = [ "upower", "gdbus" ] [[desktop_widget]] id = "widget" @@ -39,6 +39,18 @@ entry = "widget.luau" description_key = "settings.show_label.description" default = true + [[desktop_widget.setting]] + key = "label_content" + type = "select" + label_key = "settings.label_content.label" + description_key = "settings.label_content.description" + default = "percent" + options = [ + { value = "percent", label_key = "settings.label_content.percent" }, + { value = "time", label_key = "settings.label_content.time" }, + { value = "rate", label_key = "settings.label_content.rate" }, + ] + [[desktop_widget.setting]] key = "hide_plugged" type = "bool" @@ -98,12 +110,3 @@ entry = "widget.luau" description_key = "settings.battery_name.description" default = "BAT0" visible_when = { key = "battery", values = ["custom"] } - - [[desktop_widget.setting]] - key = "refresh_interval" - type = "int" - label_key = "settings.refresh_interval.label" - description_key = "settings.refresh_interval.description" - default = 60 - min = 1 - max = 600 diff --git a/battery-widget/translations/en.json b/battery-widget/translations/en.json index 96a465f..7fdfbfd 100644 --- a/battery-widget/translations/en.json +++ b/battery-widget/translations/en.json @@ -48,6 +48,16 @@ "warning_color": { "description": "Color applied when charge is at or below noctalia's warning threshold.", "label": "Warning Color" + }, + "label_content": { + "label": "Label Content", + "description": "What the battery label shows: charge percentage, time remaining, or power draw.", + "percent": "Percent", + "time": "Time", + "rate": "Rate" + } + }, + "notify": { + "error": "Something went wrong. Check the Noctalia log for details." } - } } diff --git a/battery-widget/widget.luau b/battery-widget/widget.luau index 7c38ae0..ca73284 100644 --- a/battery-widget/widget.luau +++ b/battery-widget/widget.luau @@ -1,13 +1,70 @@ local glyph local color -local percent -local status +local data = { percent = 0, status = "unknown" } +local label_content = noctalia.getConfig("label_content") local warning_threshold = 20 local path = "/sys/class/power_supply/" local batName +local stateMap = { + [0] = "unknown", + [1] = "charging", + [2] = "discharging", + [3] = "empty", + [4] = "fully-charged", + [5] = "pending-charge", + [6] = "pending-discharge", +} + +local function shellEscape(raw) + return "'" .. raw:gsub("'", "'\\''") .. "'" +end + +local function notifyError() + noctalia.notifyError("Battery widget", noctalia.tr("notify.error")) +end + +if not noctalia.commandExists("gdbus") then + noctalia.log("battery-widget: gdbus not found.") + notifyError() + return +end + +local function parse(output) + data = data or {} + + local percent = output:match("'Percentage':%s*<([%d%.]+)>") + if percent then + data.percent = tonumber(percent) or 0 + end + + local state = output:match("'State':%s*") + if state then + data.status = stateMap[tonumber(state)] or "unknown" + end + + if label_content == "time" then + local timeToEmpty = output:match("'TimeToEmpty':%s*") + if timeToEmpty then + data.timeToEmpty = tonumber(timeToEmpty) + end + + local timeToFull = output:match("'TimeToFull':%s*") + if timeToFull then + data.timeToFull = tonumber(timeToFull) + end + elseif label_content == "rate" then + local rate = output:match("'EnergyRate':%s*<([%-%d%.]+)>") + if rate then + data.rate = tonumber(rate) + end + end + + return data +end + local function getBatName() local config = noctalia.getConfig("battery") @@ -30,17 +87,17 @@ local function getBatName() end local function getGlyph() - if status == "Charging" then + if data.status == "charging" then glyph = "battery-charging" - elseif status == "Full" or status == "Not Charging" then + elseif data.status == "fully-charged" or data.status == "pending-charge" then glyph = "battery-plugged" - elseif status == "Unknown" then + elseif data.status == "unknown" then glyph = "battery-exclamation" else - glyph = percent >= 85 and "battery-4" - or percent >= 55 and "battery-3" - or percent >= 30 and "battery-2" - or percent >= 10 and "battery-1" + glyph = data.percent >= 85 and "battery-4" + or data.percent >= 55 and "battery-3" + or data.percent >= 30 and "battery-2" + or data.percent >= 10 and "battery-1" or "battery-0" end end @@ -51,36 +108,85 @@ local function getWarningThreshold() function(result) if result.exitCode ~= 0 then noctalia.log("battery-widget: failed to get warning_threshold: " .. result.stderr) + notifyError() return end local res = result.stdout - if not warning_threshold then - noctalia.log("battery-widget: invalid warning_threshold: " .. res) + if not res then + noctalia.log("battery-widget: invalid warning_threshold, keeping default") + notifyError() return end - - warning_threshold = tonumber(noctalia.string.trim(res)) + + local parsed = tonumber(noctalia.string.trim(res)) + if parsed then + warning_threshold = parsed + else + noctalia.log("battery-widget: invalid warning_threshold, keeping default") + notifyError() + end end ) end local function getColor() - if status ~= "Charging" and percent <= warning_threshold then - color = noctalia.getConfig("warning_color") + if data.status == "charging" or data.status == "fully-charged" or data.status == "pending-charge" then + color = noctalia.getConfig("charging_color") return end - if status == "Charging" or status == "Full" or status == "Not Charging" then - color = noctalia.getConfig("charging_color") + if data.percent <= warning_threshold then + color = noctalia.getConfig("warning_color") return end color = noctalia.getConfig("color") end +local function formatTime(seconds) + if not seconds or seconds <= 0 then + return nil + end + + local hours = math.floor(seconds / 3600) + local minutes = math.floor((seconds % 3600) / 60) + + if hours > 0 and minutes > 0 then + return string.format("%dh %dm", hours, minutes) + elseif hours > 0 then + return string.format("%dh", hours) + else + return string.format("%dm", minutes) + end +end + +local function formatRate(rate) + if not rate or rate <= 0 then + return nil + end + + return string.format("%.1f W", rate) +end + +local function formatPercent() + return math.floor((data.percent or 0) + 0.5) .. "%" +end + +local function getLabelText() + if label_content == "time" then + local connected = data.status == "charging" or data.status == "fully-charged" or data.status == "pending-charge" + local seconds = connected and data.timeToFull or data.timeToEmpty + return formatTime(seconds) or formatPercent() + elseif label_content == "rate" then + return formatRate(data.rate) or formatPercent() + end + + return formatPercent() +end + local function render() - if (noctalia.getConfig("hide_plugged") and (status == "Charging" or status == "Not Charging")) - or (noctalia.getConfig("hide_full") and status == "Full") then + if (noctalia.getConfig("hide_plugged") and (data.status == "charging" or data.status == "pending-charge")) + or (noctalia.getConfig("hide_full") and data.status == "fully-charged") then desktopWidget.render(ui.row()) return end @@ -90,7 +196,7 @@ local function render() table.insert(content, ui.glyph({ name = glyph, color = color })) end if noctalia.getConfig("show_label") then - table.insert(content, ui.label({ text = percent .. "%", color = color })) + table.insert(content, ui.label({ text = getLabelText(), color = color })) end local vertical = noctalia.getConfig("layout") == "vertical" @@ -105,38 +211,56 @@ local function render() ) end -local function shellEscape(raw) - return "'" .. raw:gsub("'", "'\\''") .. "'" +local function refreshBattery(output) + parse(output) + getGlyph() + getColor() + render() end -local function refreshBattery() - noctalia.runAsync( - "cat " - .. shellEscape(path .. batName .. "/capacity") - .. " " - .. shellEscape(path .. batName .. "/status"), +local function initData() + local cmd = string.format( + "gdbus call --system --dest org.freedesktop.UPower --object-path %s --method org.freedesktop.DBus.Properties.GetAll org.freedesktop.UPower.Device", + shellEscape("/org/freedesktop/UPower/devices/battery_" .. batName) + ) + noctalia.runAsync(cmd, function(result) if result.exitCode ~= 0 then noctalia.log("battery-widget: failed to read battery: " .. result.stderr) return end - local lines = {} - for line in result.stdout:gmatch("[^\r\n]+") do - table.insert(lines, line) - end - - if not lines[1] or not lines[2] then + if not result.stdout then noctalia.log("battery-widget: invalid battery output") return end - percent = tonumber(noctalia.string.trim(lines[1])) - status = noctalia.string.trim(lines[2]) + refreshBattery(result.stdout) + end + ) +end - getGlyph() - getColor() - render() +local function watchBattery() + local patterns = { "Percentage", "State" } + if label_content == "time" then + table.insert(patterns, "TimeToEmpty") + table.insert(patterns, "TimeToFull") + elseif label_content == "rate" then + table.insert(patterns, "EnergyRate") + end + + local cmd = string.format( + "gdbus monitor --system --dest org.freedesktop.UPower --object-path %s", + shellEscape("/org/freedesktop/UPower/devices/battery_" .. batName) + ) + noctalia.runStream(cmd, + function(line) + for _, pattern in ipairs(patterns) do + if line:find(pattern) then + refreshBattery(line) + break + end + end end ) end @@ -144,12 +268,11 @@ end batName = getBatName() if not batName then noctalia.log("battery-widget: battery not found") + notifyError() return end getWarningThreshold() -function update() - noctalia.setUpdateInterval(noctalia.getConfig("refresh_interval") * 1000) - refreshBattery() -end +initData() +watchBattery()