diff --git a/config/awesome/rc.lua b/config/awesome/rc.lua index ee27aefc..dbed0b13 100644 --- a/config/awesome/rc.lua +++ b/config/awesome/rc.lua @@ -16,6 +16,8 @@ local menubar = require("menubar") local hotkeys_popup = require("awful.hotkeys_popup") -- Enable hotkeys help widget for VIM and other apps when client with a matching name is opened: require("awful.hotkeys_popup.keys") +-- widgets +local battery_widget = require("widgets.battery") -- {{{ Error handling -- Check if awesome encountered an error during startup and fell back to @@ -222,6 +224,29 @@ awful.screen.connect_for_each_screen(function(s) wibox.widget.systray(), mytextclock, s.mylayoutbox, + battery_widget { + ac = "AC", + adapter = "BAT1", + ac_prefix = "AC: ", + battery_prefix = "Bat: ", + percent_colors = { + { 25, "red" }, + { 50, "orange" }, + { 999, "green" }, + }, + listen = true, + timeout = 10, + widget_text = "${AC_BAT}${color_on}${percent}%${color_off}", + widget_font = "Jetbrains Mono 16", + tooltip_text = "Battery ${state}${time_est}\nCapacity: ${capacity_percent}%", + alert_threshold = 5, + alert_timeout = 0, + alert_title = "Low battery !", + alert_text = "${AC_BAT}${time_est}", + --alert_icon = "~/Downloads/low_battery_icon.png", + warn_full_battery = true, + --full_battery_icon = "~/Downloads/full_battery_icon.png", + }, }, } end) diff --git a/config/awesome/widgets/battery.lua b/config/awesome/widgets/battery.lua new file mode 100644 index 00000000..4760d842 --- /dev/null +++ b/config/awesome/widgets/battery.lua @@ -0,0 +1,310 @@ +-- 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, +})