From cd341a92cabee6918055a6d16ea129b1d2923d2f Mon Sep 17 00:00:00 2001 From: "Eamon Burns (Laptop Powershell)" Date: Sat, 22 Jun 2024 10:59:31 -0700 Subject: [PATCH 01/10] Convert powershell output to json for easier parsing Hard-code and rename win32_battery_status_info --- lua/battery/powershell.lua | 81 +++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 44 deletions(-) diff --git a/lua/battery/powershell.lua b/lua/battery/powershell.lua index 2501834..ed20ebd 100644 --- a/lua/battery/powershell.lua +++ b/lua/battery/powershell.lua @@ -4,64 +4,57 @@ local L = require("plenary.log") local log = L.new({ plugin = "battery" }) --- Info about battery based on Status field of win32 Battery +-- Whether the AC power is connected based on Status field of win32 Battery -- see https://powershell.one/wmi/root/cimv2/win32_battery#battery-status --- 3rd field is whether AC is attached or not. nil for "who knows?" -- Note that I'm guessing here. -local win32_battery_status_info = { - { "Battery Power", false }, - { "AC Power", true }, - { "Fully Charged", true }, - { "Low", true }, - { "Critical", false }, - { "Charging", true }, - { "Charging and High", true }, - { "Charging and Low", true }, - { "Charging and Critical", true }, - { "Undefined", nil }, - { "Partially Charged", true }, +local status_code_to_ac_power = { + [1] = false, -- Battery Power + [2] = true, -- AC Power + [3] = true, -- Fully Charged + [4] = false, -- Low + [5] = false, -- Critical + [6] = true, -- Charging + [7] = true, -- Charging and High + [8] = true, -- Charging and Low + [9] = true, -- Charging and Critical + [10] = false, -- Undefined, we don't know so let's assume false + [11] = true, -- Partially Charged } +-- For a laptop with two batteries, the returned json would be in this format: +-- [ +-- { +-- "EstimatedChargeRemaining": 93, +-- "BatteryStatus": 2 +-- }, +-- { +-- "EstimatedChargeRemaining": 93, +-- "BatteryStatus": 2 +-- } +-- ] local get_battery_info_powershell_command = { - "Get-CimInstance -ClassName Win32_Battery | Select-Object -Property EstimatedChargeRemaining,BatteryStatus", + "ConvertTo-Json @(Get-CimInstance -ClassName Win32_Battery | Select-Object -Property EstimatedChargeRemaining,BatteryStatus)", } --- TODO would be nice to unit test the parser ---[[ Sample output: -{ "", - "EstimatedChargeRemaining BatteryStatus", - "------------------------ -------------", - " 92 1", - "", - "" } -]] --- - --- Parse the response from the batter info job and update +-- Parse the response json from the battery info job and update -- the battery status local function parse_powershell_battery_info(result, battery_status) - local count = 0 + -- Decode the json response into a list of batteries + local batteries = vim.json.decode(table.concat(result, "")) + local count = #batteries -- The count is just the length of batteries local charge_total = 0 local ac_power = nil - for _, line in ipairs(result) do - local found, _, charge, status = line:find("(%d+)%s+(%d+)") - if found then - count = count + 1 - charge_total = charge_total + tonumber(charge) - -- only the first battery is used to determine charging or not - -- since they should all be the same - if not ac_power then - local info = win32_battery_status_info[status] - if info ~= nil and info[2] ~= nil then - ac_power = info[2] - else - ac_power = false -- we don't know so let's guess no - end - end - end + -- Add up total charge + for _, b in ipairs(batteries) do + charge_total = charge_total + b["EstimatedChargeRemaining"] end + -- only the first battery is used to determine charging or not + -- since they should all be the same + local status = batteries[1]["BatteryStatus"] + ac_power = status_code_to_ac_power[status] + if count > 0 then battery_status.percent_charge_remaining = math.floor(charge_total / count) battery_status.battery_count = count From 535aa30d47066885d1a4ed3a9921f3e64a4dc1ac Mon Sep 17 00:00:00 2001 From: "Eamon Burns (Laptop WSL)" Date: Sat, 22 Jun 2024 16:15:38 -0700 Subject: [PATCH 02/10] feat(power_supply): add parser for power_supply files --- lua/battery/battery.lua | 4 ++ lua/battery/powersupply.lua | 73 +++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 lua/battery/powersupply.lua diff --git a/lua/battery/battery.lua b/lua/battery/battery.lua index 633b2de..808a38c 100644 --- a/lua/battery/battery.lua +++ b/lua/battery/battery.lua @@ -3,6 +3,7 @@ local M = {} local L = require("plenary.log") local powershell = require("battery.powershell") local pmset = require("battery.pmset") +local powersupply = require("battery.powersupply") local acpi = require("battery.acpi") local config = require("battery.config") @@ -73,6 +74,9 @@ local function select_job() elseif vim.fn.executable("pmset") == 1 then log.debug("pmset battery job") return pmset.get_battery_info_job + elseif vim.fn.isdirectory("/sys/class/power_supply/") then + log.debug("/sys/class/power_supply/ battery job") + return powersupply.get_battery_info_job elseif vim.fn.executable("acpi") == 1 then log.debug("acpi battery job") return acpi.get_battery_info_job diff --git a/lua/battery/powersupply.lua b/lua/battery/powersupply.lua new file mode 100644 index 0000000..737d4b2 --- /dev/null +++ b/lua/battery/powersupply.lua @@ -0,0 +1,73 @@ +-- Get battery info using /sys/class/power_supply/* files. Requires Linux + +local J = require("plenary.job") +local L = require("plenary.log") +local BC = require("util.chooser") +local config = require("battery.config") +local log = L.new({ plugin = "battery" }) + +-- Convert lowercase status from `/sys/class/power_supply/BAT?/status` +-- to whether AC power is connected +local status_to_ac_power = { + ["full"] = true, + ["charging"] = true, + ["discharging"] = false, + ["unknown"] = false, +} + +-- Parse the response from the battery info job and update +-- the battery status +local function parse_powersupply_battery_info(battery_paths, battery_status) + local count = #battery_paths + + if count > 0 then + -- Set battery count + battery_status.battery_count = count + + -- Read capacity file of first battery + local f = io.open(battery_paths[1] .. "/capacity", "r") + if not f then + return -- File doesn't exist + end + battery_status.percent_charge_remaining = f:read("n") + f:close() + + -- Read status file of first battery + f = io.open(battery_paths[1] .. "/status", "r") + if not f then + return -- File doesn't exist + end + local status = f:read("l"):lower() -- Read line (without newline character), to lowercase + battery_status.ac_power = status_to_ac_power[status] + f:close() + else + battery_status.percent_charge_remaining = 100 + battery_status.battery_count = count + battery_status.ac_power = true + end +end + +local function get_battery_info_job(battery_status) + return J:new({ + -- Find symbolic links in /sys/class/power_supply that start with BAT + -- These are the directories containing information files for each battery + command = "find", + args = { + "/sys/class/power_supply/", + "-type", "l", + "-name", "BAT*", + }, + on_exit = function (j, return_value) + if return_value == 0 then + parse_powersupply_battery_info(j:result(), battery_status) + log.debug(vim.inspect(battery_status)) + else + log.error(vim.inspect(j:result())) + end + end + }) +end + +return { + get_battery_info_job = get_battery_info_job +} From cbfe635858c01acd1fc21f3c968a41742a79023b Mon Sep 17 00:00:00 2001 From: "Eamon Burns (Laptop WSL)" Date: Sat, 22 Jun 2024 16:33:12 -0700 Subject: [PATCH 03/10] fix(powershell): shorten long line, don't overwrite unused variable --- lua/battery/powershell.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/battery/powershell.lua b/lua/battery/powershell.lua index ed20ebd..77245af 100644 --- a/lua/battery/powershell.lua +++ b/lua/battery/powershell.lua @@ -33,7 +33,8 @@ local status_code_to_ac_power = { -- } -- ] local get_battery_info_powershell_command = { - "ConvertTo-Json @(Get-CimInstance -ClassName Win32_Battery | Select-Object -Property EstimatedChargeRemaining,BatteryStatus)", + "ConvertTo-Json @(Get-CimInstance -ClassName Win32_Battery | \ + Select-Object -Property EstimatedChargeRemaining,BatteryStatus)", } -- Parse the response json from the battery info job and update @@ -43,7 +44,6 @@ local function parse_powershell_battery_info(result, battery_status) local batteries = vim.json.decode(table.concat(result, "")) local count = #batteries -- The count is just the length of batteries local charge_total = 0 - local ac_power = nil -- Add up total charge for _, b in ipairs(batteries) do @@ -53,7 +53,7 @@ local function parse_powershell_battery_info(result, battery_status) -- only the first battery is used to determine charging or not -- since they should all be the same local status = batteries[1]["BatteryStatus"] - ac_power = status_code_to_ac_power[status] + local ac_power = status_code_to_ac_power[status] if count > 0 then battery_status.percent_charge_remaining = math.floor(charge_total / count) From a2310cd40e5abe74a1cc481a2a87364eb138abac Mon Sep 17 00:00:00 2001 From: "Eamon Burns (Laptop WSL)" Date: Sat, 22 Jun 2024 18:32:33 -0700 Subject: [PATCH 04/10] fix(powersupply): remove unused variables --- lua/battery/powersupply.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/lua/battery/powersupply.lua b/lua/battery/powersupply.lua index 737d4b2..5ff8e6c 100644 --- a/lua/battery/powersupply.lua +++ b/lua/battery/powersupply.lua @@ -2,8 +2,6 @@ local J = require("plenary.job") local L = require("plenary.log") -local BC = require("util.chooser") -local config = require("battery.config") local log = L.new({ plugin = "battery" }) -- Convert lowercase status from `/sys/class/power_supply/BAT?/status` From e3625406286b1e154297b1724f434d2515d7b9d9 Mon Sep 17 00:00:00 2001 From: "Eamon Burns (Laptop WSL)" Date: Wed, 26 Jun 2024 00:09:29 -0700 Subject: [PATCH 05/10] feat(powersupply): use battery chooser to choose battery _properly_ check if `/sys/class/power_supply/` exists using `== 1` --- lua/battery/battery.lua | 2 +- lua/battery/powersupply.lua | 52 +++++++++++++++++++++++-------------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/lua/battery/battery.lua b/lua/battery/battery.lua index 808a38c..93ac755 100644 --- a/lua/battery/battery.lua +++ b/lua/battery/battery.lua @@ -74,7 +74,7 @@ local function select_job() elseif vim.fn.executable("pmset") == 1 then log.debug("pmset battery job") return pmset.get_battery_info_job - elseif vim.fn.isdirectory("/sys/class/power_supply/") then + elseif vim.fn.isdirectory("/sys/class/power_supply/") == 1 then log.debug("/sys/class/power_supply/ battery job") return powersupply.get_battery_info_job elseif vim.fn.executable("acpi") == 1 then diff --git a/lua/battery/powersupply.lua b/lua/battery/powersupply.lua index 5ff8e6c..47eea23 100644 --- a/lua/battery/powersupply.lua +++ b/lua/battery/powersupply.lua @@ -2,6 +2,8 @@ local J = require("plenary.job") local L = require("plenary.log") +local BC = require("util.chooser") +local config = require("battery.config") local log = L.new({ plugin = "battery" }) -- Convert lowercase status from `/sys/class/power_supply/BAT?/status` @@ -10,38 +12,50 @@ local status_to_ac_power = { ["full"] = true, ["charging"] = true, ["discharging"] = false, - ["unknown"] = false, + ["unknown"] = false, -- We don't know, so assume false } -- Parse the response from the battery info job and update -- the battery status local function parse_powersupply_battery_info(battery_paths, battery_status) - local count = #battery_paths + local path_count = #battery_paths + local battery_count = 0 - if count > 0 then - -- Set battery count - battery_status.battery_count = count - - -- Read capacity file of first battery - local f = io.open(battery_paths[1] .. "/capacity", "r") - if not f then - return -- File doesn't exist + if path_count > 0 then + -- Read capacities of each battery + local percents = {} + for _, path in ipairs(battery_paths) do + local f = io.open(path .. "/capacity", "r") + if f then + local charge = f:read("n") + if charge then + battery_count = battery_count + 1 + table.insert(percents, charge) + end + f:close() + end end - battery_status.percent_charge_remaining = f:read("n") - f:close() -- Read status file of first battery - f = io.open(battery_paths[1] .. "/status", "r") - if not f then - return -- File doesn't exist + local f = io.open(battery_paths[1] .. "/status", "r") + local status + if f then + -- Read line (without newline character, with a default of unknown), to lowercase + status = (f:read("l") or "unknown"):lower() + f:close() + else + status = "unknown" end - local status = f:read("l"):lower() -- Read line (without newline character), to lowercase battery_status.ac_power = status_to_ac_power[status] - f:close() + -- Choose a percent + local chosen_percent = BC.battery_chooser(percents, config.current.multiple_battery_selection) + battery_status.percent_charge_remaining = chosen_percent + -- Set battery count + battery_status.battery_count = battery_count else - battery_status.percent_charge_remaining = 100 - battery_status.battery_count = count battery_status.ac_power = true + battery_status.percent_charge_remaining = 100 + battery_status.battery_count = path_count end end From 2ec83dea054c47ab287263d5fb3bfb117171a69a Mon Sep 17 00:00:00 2001 From: "Eamon Burns (Laptop WSL)" Date: Wed, 26 Jun 2024 10:25:59 -0700 Subject: [PATCH 06/10] feat(powersupply): also check if power_supply directory is readable --- lua/battery/battery.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lua/battery/battery.lua b/lua/battery/battery.lua index 93ac755..423872d 100644 --- a/lua/battery/battery.lua +++ b/lua/battery/battery.lua @@ -74,7 +74,10 @@ local function select_job() elseif vim.fn.executable("pmset") == 1 then log.debug("pmset battery job") return pmset.get_battery_info_job - elseif vim.fn.isdirectory("/sys/class/power_supply/") == 1 then + -- Ensure directory exists (isdirectory() == 1) and is readable ($(ls) doesn't contain "ls: cannot") + elseif vim.fn.isdirectory("/sys/class/power_supply/") == 1 and + -- TODO: Find better way to check for readability (exit code of `ls`?) + not vim.fn.system("ls /sys/class/power_supply/"):find("ls: cannot") then log.debug("/sys/class/power_supply/ battery job") return powersupply.get_battery_info_job elseif vim.fn.executable("acpi") == 1 then From 026ed9a0890d44e722569fea70ac2600155f5d02 Mon Sep 17 00:00:00 2001 From: Justin Heyes-Jones Date: Sun, 30 Jun 2024 11:19:24 -0700 Subject: [PATCH 07/10] rename health module code (#28) Co-authored-by: justinhj-td --- lua/battery/health/{health.lua => init.lua} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lua/battery/health/{health.lua => init.lua} (100%) diff --git a/lua/battery/health/health.lua b/lua/battery/health/init.lua similarity index 100% rename from lua/battery/health/health.lua rename to lua/battery/health/init.lua From da3f59681920925f5eab324a78df518c9f54c99c Mon Sep 17 00:00:00 2001 From: Justin Heyes-Jones Date: Sun, 30 Jun 2024 11:32:00 -0700 Subject: [PATCH 08/10] Issue 26 health module (#29) * rename health module code * maybe this is better --------- Co-authored-by: justinhj-td --- lua/battery/health.lua | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 lua/battery/health.lua diff --git a/lua/battery/health.lua b/lua/battery/health.lua new file mode 100644 index 0000000..428a370 --- /dev/null +++ b/lua/battery/health.lua @@ -0,0 +1,26 @@ +local M = {} + +local start = vim.health.start or vim.health.report_start +local ok = vim.health.ok or vim.health.report_ok +local error = vim.health.error or vim.health.report_error + +-- TODO should check if the plugin is initialized and the setup is valid +-- TODO should check what method is being used for battery information + +local function check_setup() + return true +end + +M.check = function() + start("battery report") + -- make sure setup function parameters are ok + if check_setup() then + ok("Setup function is correct") + else + error("Setup function is incorrect") + end + -- do some more checking + -- ... +end + +return M From dfe2ff5786f21a01cec383575094e550aa34ee96 Mon Sep 17 00:00:00 2001 From: justinhj-td Date: Sun, 30 Jun 2024 11:33:47 -0700 Subject: [PATCH 09/10] remove unused file --- lua/battery/health/init.lua | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 lua/battery/health/init.lua diff --git a/lua/battery/health/init.lua b/lua/battery/health/init.lua deleted file mode 100644 index 428a370..0000000 --- a/lua/battery/health/init.lua +++ /dev/null @@ -1,26 +0,0 @@ -local M = {} - -local start = vim.health.start or vim.health.report_start -local ok = vim.health.ok or vim.health.report_ok -local error = vim.health.error or vim.health.report_error - --- TODO should check if the plugin is initialized and the setup is valid --- TODO should check what method is being used for battery information - -local function check_setup() - return true -end - -M.check = function() - start("battery report") - -- make sure setup function parameters are ok - if check_setup() then - ok("Setup function is correct") - else - error("Setup function is incorrect") - end - -- do some more checking - -- ... -end - -return M From dca736dd50de31b74869360f26bb6283c0c74c9b Mon Sep 17 00:00:00 2001 From: justinhj-td Date: Sun, 30 Jun 2024 20:05:27 -0700 Subject: [PATCH 10/10] rebase and add readable directory predicate --- .luacheckrc | 2 +- README.md | 4 ++-- lua/battery/battery.lua | 36 +++++++++++++++++++++++++----------- lua/battery/health.lua | 18 +++++++----------- lua/util/file.lua | 10 ++++++++++ 5 files changed, 45 insertions(+), 25 deletions(-) create mode 100644 lua/util/file.lua diff --git a/.luacheckrc b/.luacheckrc index 653f105..a546664 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1 +1 @@ -globals = { "vim" } +globals = { "vim", "_" } diff --git a/README.md b/README.md index e0cf971..0601e4d 100644 --- a/README.md +++ b/README.md @@ -122,9 +122,9 @@ If something breaks you should see a standard Vim error telling you what the pro For more than just info,warn and error logging you can enable debug logs which show a more verbose behaviour of the plugin using the following command to launch nvim. -`PLENARY_DEBUG=true nvim` +`DEBUG_PLENARY=true nvim` ## Notes Inspired by [lambdalisue/battery.vim](https://github.com/lambdalisue/battery.vim), which in turn uses code from [b4b4r07/dotfiles](https://github.com/b4b4r07/dotfiles/blob/66dddda6803ada50a0ab879e5db784afea72b7be/.tmux/bin/battery#L10). -Copyright (c) 2022 Justin Heyes-Jones +Copyright (c) 2022-2024 Justin Heyes-Jones diff --git a/lua/battery/battery.lua b/lua/battery/battery.lua index 423872d..5b155c0 100644 --- a/lua/battery/battery.lua +++ b/lua/battery/battery.lua @@ -6,6 +6,7 @@ local pmset = require("battery.pmset") local powersupply = require("battery.powersupply") local acpi = require("battery.acpi") local config = require("battery.config") +local file = require("util.file") -- TODO check for icons and if not available fallback to text -- TODO allow user to select no icons @@ -53,6 +54,7 @@ local battery_status = { percent_charge_remaining = nil, battery_count = nil, ac_power = nil, + method = nil, } -- Gets the last updated battery information @@ -70,28 +72,37 @@ local timer = nil local function select_job() if vim.fn.has("win32") and vim.fn.executable("powershell") == 1 then log.debug("windows powershell battery job") - return powershell.get_battery_info_job + return powershell.get_battery_info_job, 'powershell' elseif vim.fn.executable("pmset") == 1 then log.debug("pmset battery job") - return pmset.get_battery_info_job - -- Ensure directory exists (isdirectory() == 1) and is readable ($(ls) doesn't contain "ls: cannot") - elseif vim.fn.isdirectory("/sys/class/power_supply/") == 1 and - -- TODO: Find better way to check for readability (exit code of `ls`?) - not vim.fn.system("ls /sys/class/power_supply/"):find("ls: cannot") then - log.debug("/sys/class/power_supply/ battery job") - return powersupply.get_battery_info_job + return pmset.get_battery_info_job, 'pmset' + elseif file.is_readable_directory("/sys/class/power_supply/") then + log.debug("power_supply battery job") + return powersupply.get_battery_info_job, 'powersupply' elseif vim.fn.executable("acpi") == 1 then log.debug("acpi battery job") - return acpi.get_battery_info_job + return acpi.get_battery_info_job, 'acpi' else log.debug("no battery job") + return nil, 'none' end end +-- This is used for the health check +local function get_method() + local method = battery_status.method + if method == nil then + _, method = select_job() + end + return method +end + local function timer_loop() vim.defer_fn(function() log.debug(timer .. " is running now") - local job_function = select_job() + local job_function, method = select_job() + battery_status.method = method + log.debug("using method " .. method) if job_function then job_function(battery_status):start() @@ -119,7 +130,9 @@ local function start_timer() timer = require("util.timers").get_next() -- Always call the job immediately before starting the timed loop - local job_function = select_job() + local job_function, method = select_job() + battery_status.method = method + log.debug("using method " .. method) if job_function then job_function(battery_status):start() @@ -203,4 +216,5 @@ end M.setup = setup M.get_battery_status = get_battery_status M.get_status_line = get_status_line +M.get_method = get_method return M diff --git a/lua/battery/health.lua b/lua/battery/health.lua index 428a370..bc08e36 100644 --- a/lua/battery/health.lua +++ b/lua/battery/health.lua @@ -4,23 +4,19 @@ local start = vim.health.start or vim.health.report_start local ok = vim.health.ok or vim.health.report_ok local error = vim.health.error or vim.health.report_error --- TODO should check if the plugin is initialized and the setup is valid --- TODO should check what method is being used for battery information +local B = require("battery.battery") -local function check_setup() - return true +local function check_method() + return B.get_method() ~= nil end M.check = function() - start("battery report") - -- make sure setup function parameters are ok - if check_setup() then - ok("Setup function is correct") + start("Checking method to get battery status") + if check_method() then + ok("Using " .. B.get_method() .. "") else - error("Setup function is incorrect") + error("No method found.") end - -- do some more checking - -- ... end return M diff --git a/lua/util/file.lua b/lua/util/file.lua new file mode 100644 index 0000000..7784b07 --- /dev/null +++ b/lua/util/file.lua @@ -0,0 +1,10 @@ +local bit = require('bit') + +local function is_readable_directory(file) + local s = vim.loop.fs_stat(file) + return s ~= nil and s.type == 'directory' and bit.band(s.mode, 4) == 4 +end + +return { + is_readable_directory = is_readable_directory, +}