-- Battery widget local awful = require("awful") local gears = require("gears") local wibox = require("wibox") local naughty = require("naughty") local timer = gears.timer or timer local watch = awful.spawn and awful.spawn.with_line_callback ------------------------------------------ -- Private utility functions ------------------------------------------ local tolower = string.lower local function file_exists(command) local f = io.open(command) if f then f:close() end return f and true or false end local function readfile(command) local file = io.open(command) if not file then return nil end local text = file:read('*all') file:close() return text end local function color_tags(color) if color then return '', '' else return '', '' end end local function round(value) return math.floor(value + 0.5) end local function trim(s) if not s then return nil end return (s:gsub("^%s*(.-)%s*$", "%1")) end local function read_trim(filename) return trim(readfile(filename)) or "" end local function substitute(template, context) if type(template) == "string" then return (template:gsub("%${([%w_]+)}", function(key) return tostring(context[key] or "Err!") end)) else -- function / functor: return template(context) end end local function lookup_by_limits(limits, value) if type(limits) == "table" then local last = nil if value then for k, v in ipairs(limits) do if (value <= v[1]) then return v[2] end last = v[2] end end return last else return limits end end ------------------------------------------ -- Battery widget interface ------------------------------------------ local battery_widget = {} local sysfs_names = { charging = { present = "present", state = "status", rate = "current_now", charge = "charge_now", capacity = "charge_full", design = "charge_full_design", percent = "capacity", }, discharging = { present = "present", state = "status", rate = "power_now", charge = "energy_now", capacity = "energy_full", design = "energy_full_design", percent = "capacity" }, } function battery_widget:new(args) if args.adapter then return setmetatable({}, { __index = self }):init(args) end -- creates an empty container wibox, which can be added to your panel even if its empty local widgets = { layout = wibox.layout.fixed.horizontal } local batteries, mains, usb, ups = self:discover() local ac = mains[1] or usb[1] or ups[1] for i, adapter in ipairs(batteries) do local _args = setmetatable({ adapter = adapter, ac = ac }, { __index = args }) table.insert(widgets, self(_args).widget) end return widgets end function battery_widget:discover() local pow = "/sys/class/power_supply/" local adapters = { Battery = {}, UPS = {}, Mains = {}, USB = {} } for adapter in io.popen("ls -1 " .. pow):lines() do local type = read_trim(pow .. adapter .. "/type") table.insert(adapters[type], adapter) end return adapters.Battery, adapters.Mains, adapters.USB, adapters.UPS end function battery_widget:init(args) self.ac = args.ac or "AC" self.adapter = args.adapter or "BAT0" self.ac_prefix = args.ac_prefix or "AC: " self.battery_prefix = args.battery_prefix or "Bat: " self.percent_colors = args.percent_colors or args.limits or { { 25, "red" }, { 50, "orange" }, { 999, "green" }, } self.widget_text = args.widget_text or ( "${AC_BAT}${color_on}${percent}%${color_off}") self.tooltip_text = args.tooltip_text or ( "Battery ${state}${time_est}\nCapacity: ${capacity_percent}%") self.alert_threshold = args.alert_threshold or 5 self.alert_timeout = args.alert_timeout or 0 self.alert_title = args.alert_title or "Low battery !" self.alert_text = args.alert_text or "${AC_BAT}${time_est}" self.alert_icon = args.alert_icon or nil self.widget = wibox.widget.textbox() self.widget.font = args.widget_font self.tooltip = awful.tooltip({ objects = { self.widget } }) self.warn_full_battery = args.warn_full_battery self.full_battery_icon = args.full_battery_icon or nil self.widget:buttons(awful.util.table.join( awful.button({}, 1, function() self:update() end), awful.button({}, 3, function() self:update() end) )) self.timer = timer({ timeout = args.timeout or 10 }) self.timer:connect_signal("timeout", function() self:update() end) self.timer:start() self:update() if (args.listen or args.listen == nil) and watch then self.listener = watch("acpi_listen", { stdout = function(line) self:update() end, }) awesome.connect_signal("exit", function() awesome.kill(self.listener, awesome.unix_signal.SIGTERM) end) end return self end function battery_widget:get_state() local pow = "/sys/class/power_supply/" local ac = pow .. self.ac local bat = pow .. self.adapter local sysfs = (file_exists(bat .. "/" .. sysfs_names.charging.rate) and sysfs_names.charging or sysfs_names.discharging) -- If there is no battery on this machine. if not sysfs.state then return nil end -- return value local r = { state = tolower(read_trim(bat .. "/" .. sysfs.state)), present = tonumber(read_trim(bat .. "/" .. sysfs.present)), rate = tonumber(read_trim(bat .. "/" .. sysfs.rate)), charge = tonumber(read_trim(bat .. "/" .. sysfs.charge)), capacity = tonumber(read_trim(bat .. "/" .. sysfs.capacity)), design = tonumber(read_trim(bat .. "/" .. sysfs.design)), percent = tonumber(read_trim(bat .. "/" .. sysfs.percent)), } r.ac_state = tonumber(read_trim(ac .. "/online")) if r.state == "unknown" then r.state = "charged" end if r.percent == nil and r.charge and r.capacity then r.percent = round(r.charge * 100 / r.capacity) end return r end function battery_widget:update() local ctx = self:get_state() -- If there is no battery on this machine. if not ctx then return nil end -- AC/battery prefix ctx.AC_BAT = (ctx.ac_state == 1 and lookup_by_limits(self.ac_prefix, ctx.percent) or lookup_by_limits(self.battery_prefix, ctx.percent) or "Err!") -- Colors ctx.color_on, ctx.color_off = color_tags( lookup_by_limits(self.percent_colors, ctx.percent)) -- estimate time ctx.charge_dir = 0 -- +1|0|-1 -> charging|static|discharging ctx.time_left = nil -- time until charging/discharging complete ctx.time_text = "" ctx.time_est = "" if ctx.rate and ctx.rate ~= 0 then if not ctx.state or ctx.state == "discharging" then ctx.charge_dir = -1 ctx.time_left = ctx.charge / ctx.rate elseif ctx.state == "charging" then ctx.charge_dir = 1 ctx.time_left = (ctx.capacity - ctx.charge) / ctx.rate end end if ctx.time_left then ctx.hours = math.floor((ctx.time_left)) ctx.minutes = math.floor((ctx.time_left - ctx.hours) * 60) if ctx.hours > 0 then ctx.time_text = ctx.hours .. "h " .. ctx.minutes .. "m" else ctx.time_text = ctx.minutes .. "m" end ctx.time_est = ": " .. ctx.time_text .. " remaining" end -- capacity text if ctx.capacity and ctx.design then ctx.capacity_percent = round(ctx.capacity / ctx.design * 100) end -- for use in functions ctx.obj = self -- update text self.widget:set_markup(substitute(self.widget_text, ctx)) self.tooltip:set_text(substitute(self.tooltip_text, ctx)) -- low battery notification if naughty then if (ctx.state == "discharging" and ctx.percent and ctx.percent <= self.alert_threshold) then self:notify(substitute(self.alert_title, ctx), substitute(self.alert_text, ctx), self.alert_icon) elseif ctx.state == "full" and self.warn_full_battery then self:notify('Battery Full!', 'Remove power cord', self.full_battery_icon) else if self.alert then naughty.destroy( self.alert, naughty.notificationClosedReason.dismissedByCommand) self.alert = nil end end end end function battery_widget:notify(title, text, icon) if self.alert then naughty.replace_text(self.alert, title, text) else self.alert = naughty.notify({ title = title, text = text, icon = icon, preset = naughty.config.presets.critical, timeout = self.alert_timeout }) end end return setmetatable(battery_widget, { __call = battery_widget.new, })