-- utility functions and variables local utils = {} -- are we using Lua 5.1 (or below) utils.lua_5_1 = (_VERSION <= "Lua 5.1") -- shim functions utils.loadstring = utils.lua_5_1 and loadstring or load -- dump object -- recursively dumps tables, with an optional depth limir (unlimited by default) -- cf: https://stackoverflow.com/a/27028488/707516 function utils.dump_object(o, depth) if depth ~= nil and depth >= 0 then depth = depth - 1 end if type(o) == "table" then if depth < 0 then return "{...}" end local s = "{" local c = 0 for k, v in pairs(o) do c = c + 1 if type(k) ~= "number" then k = '"' .. k .. '"' end s = s .. " [" .. k .. "] = " .. utils.dump_object(v, depth) .. "," end if c > 0 then s = s:sub(1, -2) .. " " end return s .. "}" else local s = tostring(o) if type(o) == "string" then s = '"' .. s .. '"' end return s end end -- table utilities utils.table = { unpack = utils.lua_5_1 and unpack or table.unpack } -- update `dst` table by merging the other `src` table -- `overwrite`: if true (default), overwrite existing `dst` entries with values -- from `src`, otherwise only merge those not already existing function utils.table.update(dst, src, overwrite) if overwrite == nil then overwrite = true end if src then for k, v in pairs(src) do if overwrite or dst[k] == nil then dst[k] = v end end end return dst end -- get value with default function utils.table.get(t, k, default) if t == nil then t = {} end local v = t[k] if v == nil then return default end return v end -- pop values from table (as multiple returns) -- usage: local a, b = utils.table.pop({ x=1, y=2, z=3 }, 'x', 'z') -> a = 1, b = 3 function utils.table.pop(t, ...) if t == nil then t = {} end local ret = {} for _, k in ipairs(arg) do table.insert(ret, t[k]) t[k] = nil end return utils.table.unpack(ret) end -- lazy table: storing values to be evaluated on the first access -- usage: -- local lz = utils.table.lazy() -- local expensive_eval = function(t) return ... end -- argument `t` is optional -- lz.foo = expensive_eval function utils.table.lazy(vars) local lazy_t = require('external.lazybag').new() getmetatable(lazy_t).__newindex = function(t, k, v) t:lazy(k, v) end for k, v in pairs(vars or {}) do lazy_t[k] = v end return lazy_t end -- load Lua file in a separate env to prevent polluting global env function utils.load_in_env(path, env) local _env = env or {} if not env then setmetatable(_env, { __index = _G }) -- global fallback end if utils.lua_5_1 then local f = loadfile(path) if not f then return {} end assert(pcall(setfenv(f, _env))) else local f = loadfile(path, 't', _env) if not f then return {} end assert(pcall(f)) end if not env then setmetatable(_env, nil) end return _env end -- enumerate network interfaces, see https://superuser.com/a/1173532/95569 function utils.enum_ifaces() local _in_docker = utils.in_docker() local ifaces = {} local iface_names = utils.sys_call("basename -a /sys/class/net/*") for i, l in ipairs(iface_names) do local p = utils.sys_call("realpath /sys/class/net/" .. l, true) -- for regular host, skip virtual interfaces (including lo) -- in container, return all interfaces except lo if not p:match("^/sys/devices/virtual/") or (_in_docker and l ~= "lo") then table.insert(ifaces, l) end end return ifaces end -- enumerate mounted disks -- by default only show essential filesystems, but customizable function utils.enum_disks(include_types, exclude_types, exclude_targets) local fs_types_default = "ext4,ext3,ext2,xfs,btrfs,zfs,ecryptfs,fuseblk,ntfs3,ntfs,vfat,exfat,fat" if utils.in_docker() then fs_types_default = fs_types_default .. ",overlay" end local fs_types = utils.clean_array( utils.str_to_array(fs_types_default .. "," .. include_types, ",", true, true), utils.str_to_array(exclude_types), true ) local cmd = "findmnt -bPUno TARGET,FSTYPE,SIZE,USED -t " .. utils.join_strs(fs_types, ",") local entry_pattern = '^TARGET="(.+)"%s+FSTYPE="(.+)"%s+SIZE="(.+)"%s+USED="(.+)"$' local mnt_fs = utils.sys_call(cmd) local mnts = {} for _, l in ipairs(mnt_fs) do local mnt, type, size, used = l:match(entry_pattern) for _, p in ipairs(utils.str_to_array(exclude_targets) or {}) do if mnt == nil or mnt:match(p) then mnt = nil break end end if mnt and utils.is_dir(mnt) and utils.is_readable(mnt) then table.insert(mnts, { mnt = mnt, type = type, size = tonumber(size), used = tonumber(used) }) end end return mnts end -- some environment variables utils.env = {} for i, k in ipairs({ "HOME", "USER" }) do utils.env[k] = os.getenv(k) end -- human friendly file size local _filesize = require("external.filesize") function utils.filesize(size) return _filesize(size, { round = 0, spacer = "", base = 2 }) end -- call at interval, similar to Conky's `execi` but for functions local _interval_call_cache = {} function utils.interval_call(interv, func, ...) if _interval_call_cache[func] == nil then _interval_call_cache[func] = {} end local cache = _interval_call_cache[func] local now = os.time() if cache.last == nil or (now - cache.last) >= interv then cache.result = func(...) cache.last = now end return cache.result end -- template renderer. usage: -- foo_tpl = tpl("this is {%= foo %}") -- foo_tpl{foo = "bar"} -> "this is bar" local _liluat = require("external.liluat") function utils.tpl(t) local ct = _liluat.compile(t, { start_tag = "{%", end_tag = "%}" }) return function(values) return _liluat.render(ct, values) end end -- pad string to `max_len`, `align` mode can be 'l/left', 'r/right' or 'c/center' function utils.padding(str, max_len, align, char) if not max_len then return str end local n = max_len - utils.utf8_len(str) if n <= 0 then return str end if not align then align = "l" end if not char then char = " " end assert(utils.utf8_len(char) == 1, "padding `char` must be a single character.") local srep = string.rep if align == "c" or align == "center" then local m = math.floor(n / 2) return srep(char, m) .. str .. srep(char, n - m) elseif align == "l" or align == "left" then return str .. srep(char, n) elseif align == "r" or align == "right" then return srep(char, n) .. str end end -- strip surrounding whitespaces function utils.trim(str) return str:match("^%s*(.-)%s*$") end -- strip surrounding braces function utils.unbrace(str) if not str then return str end while true do local u = str:match("^{(.-)}$") if u then str = u else return str end end end -- count characters in a utf-8 encoded string function utils.utf8_len(str) local _, count = string.gsub(str, "[^\128-\193]", "") return count end -- split comma-separated string to array, supported separators are: ,(default) ; : - _ + -- if str is already an array (table), it is returned without processing -- if trim is true, surrounding spaces around each item are trimmed -- if ignore_empty is true, only non-empty items (after optional trimming) are added function utils.str_to_array(str, sep, trim, ignore_empty) if type(str) == "table" then return str end if type(str) ~= "string" then return nil end if sep == nil then sep = "," end if trim == nil then trim = false end if ignore_empty == nil then ignore_empty = false end if type(sep) ~= "string" or #sep ~= 1 or not sep:match("[%,%;%:%-%_%+]") then return nil end local arr = {} local p = 1 local function _append(s) if trim then s = utils.trim(s) end if #s > 0 or not ignore_empty then table.insert(arr, s) end end while true do local q = str:find(sep, p, true) if q then _append(string.sub(str, p, q - 1)) p = q + 1 else _append(string.sub(str, p)) break end end return arr end -- clean array: exclude certain items and/or remove duplicate items function utils.clean_array(arr, exclude, dedup) local exclude_hash = {} local dedup_hash = {} local cleaned = {} for _, k in ipairs(exclude) do exclude_hash[k] = true end for _, k in ipairs(arr) do if dedup and dedup_hash[k] or exclude_hash[k] then else table.insert(cleaned, k) if dedup then dedup_hash[k] = true end end end return cleaned end -- join strings stored in an array function utils.join_strs(strs, sep) return table.concat(strs, sep) end -- round float to integer or specified number of digits function utils.round(x, ndigits) ndigits = math.floor(ndigits or 0) if ndigits <= 0 then return math.floor(x + 0.5) end local pow = 10 ^ ndigits return math.floor(x * pow + 0.5) / pow end -- calculate ratio as percentage function utils.ratio_perc(x, y, ndigits) return utils.round(100.0 * tonumber(x) / tonumber(y), ndigits) end -- run system command and return stdout as lines or a string function utils.sys_call(cmd, as_string) local pipe = io.popen(cmd .. [[;echo "\n$?"]]) if not pipe then return nil, 1 end local lines = {} for l in pipe:lines() do table.insert(lines, l) end pipe:close() local return_code = tonumber(table.remove(lines)) if as_string then return table.concat(lines, "\n"), return_code else return lines, return_code end end -- eval string as system call and check if result is true function utils.is_true(expr) local s = utils.sys_call(expr .. ' && echo "true"', true) return (#s > 3) end -- is dir or file function utils.is_dir(p) return utils.is_true('[ -d "' .. p .. '" ]') end -- is path readable function utils.is_readable(p) return utils.is_true('[ -r "' .. p .. '" ]') end -- is running in a docker container function utils.in_docker() return utils.is_true('[ -f /.dockerenv ] || grep -Eq "(lxc|docker)" /proc/1/cgroup') end return utils