aboutsummaryrefslogtreecommitdiffstats
path: root/lib/data.lua
blob: 28dce1e39c5fd2259f22685a0090de177d90731b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
-- 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