-- Readers for /proc and /sys. -- -- Every function takes the file *contents* as a string rather than reading the -- file itself, so the parsers are testable against fixtures without mocking the -- filesystem. The thin read-and-parse wrappers live at the bottom. local M = {} -- Aggregate CPU jiffies from /proc/stat. Returns total, idle. -- iowait counts as idle: a core waiting on disk is not doing work, and -- treating it as busy makes a disk-bound system look CPU-bound. function M.cpu_times(stat) local line = stat:match('^cpu%s+([^\n]+)') if not line then return nil, nil end local v = {} for n in line:gmatch('%d+') do v[#v + 1] = tonumber(n) end if #v < 5 then return nil, nil end local total = 0 for _, n in ipairs(v) do total = total + n end return total, v[4] + v[5] -- idle + iowait end -- /proc/meminfo, values in kB as the file states them. function M.mem_info(meminfo) local function field(name) return tonumber(meminfo:match(name .. ':%s+(%d+)')) end local total = field('MemTotal') local available = field('MemAvailable') if not (total and available) then return nil end return { total = total, free = field('MemFree'), available = available, -- MemAvailable already excludes reclaimable cache, so this is the figure a -- user recognises as "used", unlike total-free which counts cache. used = total - available, } end -- hwmon temp*_input is millidegrees C. Returns whole degrees, or nil. -- nil rather than 0 on failure: 0 C is a legitimate reading. function M.millidegrees(s) if not s then return nil end local n = tonumber(s:match('^%s*(-?%d+)')) if not n then return nil end return math.floor(n / 1000 + 0.5) end -- Read a whole file, returning nil if it cannot be read. Used by the wrappers -- so a vanished sysfs path degrades to nil instead of raising. function M.slurp(path) local f = io.open(path, 'r') if not f then return nil end local s = f:read('*a') f:close() return s end -- Find a hwmon directory by the exact contents of its `name` file. -- -- Globbing by name, never by a fixed hwmon index: indices drift across kernel -- and hardware reorders, and a stale index silently reports a different chip. -- Carried over from the previous conky config, where this was the hard-won bit. function M.hwmon_dir(name) local p = io.popen('grep -lx ' .. ("%q"):format(name) .. ' /sys/class/hwmon/hwmon*/name 2>/dev/null') if not p then return nil end local hit = p:read('*l') p:close() if not hit then return nil end return hit:match('^(.*)/name$') end -- A stateful CPU-load counter. -- -- Load is work done between two samples, so a single reading cannot produce a -- percentage. Each counter keeps its own previous sample, which also means a -- per-core counter is just another instance. function M.new_cpu_counter() return { prev_total = nil, prev_idle = nil, -- Returns busy percent since the previous sample, or nil when there is no -- usable delta (first call, or the counters did not advance). sample = function(self, total, idle) if not (total and idle) then return nil end local pt, pi = self.prev_total, self.prev_idle self.prev_total, self.prev_idle = total, idle if not pt then return nil end local dt = total - pt if dt <= 0 then return nil end local busy = (dt - (idle - pi)) / dt * 100 -- Clamp: a counter reset or a suspend/resume can produce a nonsense -- delta, and a bar drawn at -12% or 340% is worse than a clamped one. if busy < 0 then busy = 0 elseif busy > 100 then busy = 100 end return busy end, } end return M