Battery indicator
A bar pill with a battery icon and percentage that turns red when low, a tooltip with the charge state and time left, and a desktop notification when the charge drops past 15%. On a desktop with no battery the pill hides itself.
local LOW = 15
local pill_hover = hover("battery")
local STATES = {
charging = "Charging",
discharging = "On battery",
fully_charged = "Fully charged",
pending_charge = "Plugged in, not charging",
empty = "Empty",
}
local function duration(seconds)
local hours, minutes = seconds // 3600, seconds % 3600 // 60
return hours > 0 and string.format("%d h %02d min", hours, minutes) or string.format("%d min", minutes)
end
-- Adwaita-style names: battery-level-0 to battery-level-100 in steps of 10.
local glyph = mantle.battery:map(function(battery)
if not battery.present then
return "battery-missing-symbolic"
end
local step = math.floor(battery.percent / 10 + 0.5) * 10
if battery.state == "fully_charged" or (step == 100 and battery.state == "charging") then
return "battery-level-100-charged-symbolic"
end
return string.format("battery-level-%d%s-symbolic", step, battery.state == "charging" and "-charging" or "")
end, "battery-missing-symbolic")
local colour = mantle.battery:map(function(battery)
local low = battery.present and battery.percent <= LOW and battery.state == "discharging"
return low and "#f38ba8" or "#cdd6f4"
end, "#cdd6f4")
local details = mantle.battery:map(function(battery)
if not battery.present then
return "No battery"
end
local line = string.format("%d%% · %s", battery.percent, STATES[battery.state] or battery.state)
if battery.state == "discharging" and battery.time_to_empty then
line = line .. "\n" .. duration(battery.time_to_empty) .. " left"
elseif battery.state == "charging" and battery.time_to_full then
line = line .. "\n" .. duration(battery.time_to_full) .. " to full"
end
return line
end, "No battery")
-- Warn once each time the charge falls past LOW while draining.
mantle.battery:on_change(function(battery, previous)
if previous == nil or not battery.present then
return
end
if battery.state == "discharging" and battery.percent <= LOW and previous.percent > LOW then
process.detach("notify-send", { "-u", "critical", "Battery low", battery.percent .. "% remaining" })
end
end)
local pill = row {
align_v = "center",
spacing = 4,
padding = { left = 8, right = 10, top = 3, bottom = 3 },
radius = 12,
background = pill_hover:map(function(on) return on and "#45475a" or "#313244" end),
hover = pill_hover,
visible = mantle.battery:map(function(battery) return battery.present end, false),
children = {
icon { name = glyph, size = 16, foreground = colour, align_v = "center" },
text {
content = mantle.battery:map(function(battery)
return battery.present and battery.percent .. "%" or ""
end, ""),
foreground = colour,
align_v = "center",
},
},
}
return {
panel {
id = "bar",
layer = "top",
anchor = { top = true, left = true, right = true },
width = "fill",
height = 32,
exclusive_zone = true,
child = row {
width = "fill",
height = "fill",
padding = { left = 8, right = 8 },
background = "#1e1e2e",
children = { rect { width = "fill" }, pill },
},
},
popup {
id = "battery_tooltip",
parent = "bar",
anchor_rect = hover_rect("battery"),
anchor = "bottom",
gravity = "bottom",
offset = { y = 6 },
grab = false,
visible = pill_hover,
padding = 8,
radius = 8,
background = "#1e1e2e",
border_width = 1,
border_color = "#45475a",
child = text { content = details, foreground = "#cdd6f4", wrap = "word" },
},
}
How it works
mantle.batteryreadspresent = falseon a desktop, and a map givesbeforefor the time before its first push (battery).statenames UPower’s charge state;time_to_emptyandtime_to_fullare optional and need their own guard.- The icon is picked by name from the icon theme and tinted with
foreground(icon). on_changecompares with the previous push to fire only on the downward crossing (on_change);process.detachrunsnotify-send(process.detach).- The tooltip is a non-grabbing popup anchored to
hover_rect(hover).
Variations
| Change | Edit |
|---|---|
| Charge as a bar instead of an icon | A 24 × 10 rect track with a child rect { width = battery.percent .. "%", height = "fill" } (sizes) |
| Cycle the power profile on click | Make the pill a node with an on_click that picks the next entry of mantle.power:get().profiles and calls set_profile (power) |
| Show the wattage | Add text { content = mantle.power:map(function(power) return power.energy_rate and string.format("%.1f W", power.energy_rate) or "" end, "") } |
| Different threshold | LOW = 20 |
| Hide the pill on mains at full charge | visible returns battery.present and battery.state ~= "fully_charged", with before false |