aboutsummaryrefslogtreecommitdiffstats
path: root/lib/data.lua
blob: 9bd5433978342b9d20ccc2dcd596017432ccd896 (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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
-- 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

-- Per-core jiffies from /proc/stat, in file order, so entry N is core N.
--
-- Separate from cpu_times() rather than a flag on it: the aggregate is a
-- single pair and this is a list, and a function returning one or the other
-- depending on an argument is worse than two functions.
--
-- The leading 'cpu ' aggregate is excluded by requiring a digit after 'cpu',
-- since counting it would draw an extra bar showing the average alongside the
-- real cores.
function M.per_cpu_times(stat)
  local out = {}
  if type(stat) ~= 'string' then return out end
  for line in stat:gmatch('[^\n]+') do
    local nums = line:match('^cpu%d+%s+(.+)$')
    if nums then
      local v = {}
      for n in nums:gmatch('%d+') do v[#v + 1] = tonumber(n) end
      -- Needs at least user..iowait to compute a busy fraction; a shorter line
      -- is truncated or from a kernel that reports differently, and a partial
      -- sum would look plausible while being wrong.
      if #v >= 5 then
        local total = 0
        for _, n in ipairs(v) do total = total + n end
        out[#out + 1] = { total = total, idle = v[4] + v[5] }
      end
    end
  end
  return out
end

-- A named hwmon sensor's value in whole degrees, or nil.
--
-- Wraps hwmon_dir + slurp + millidegrees so a widget names the chip and the
-- file rather than building paths. nil, never 0: zero degrees is a plausible
-- reading and must not be indistinguishable from a missing sensor.
function M.sensor(chip, file)
  if type(chip) ~= 'string' or type(file) ~= 'string' then return nil end
  local dir = M.hwmon_dir(chip)
  if not dir then return nil end
  return M.millidegrees(M.slurp(dir .. '/' .. file))
end

-- A raw hwmon integer (fan RPM, power microwatts), or nil. Same lookup as
-- sensor() without the millidegree conversion.
function M.sensor_raw(chip, file)
  if type(chip) ~= 'string' or type(file) ~= 'string' then return nil end
  local dir = M.hwmon_dir(chip)
  if not dir then return nil end
  local s = M.slurp(dir .. '/' .. file)
  if not s then return nil end
  return tonumber(s:match('^%s*(-?%d+)'))
end

return M