aboutsummaryrefslogtreecommitdiffstats
path: root/widgets/system.lua
blob: 7116302c321a59e8b78e41f61293df2e26deddd7 (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
-- System: CPU load as a per-core equaliser, memory, and every temperature.
--
-- The hwmon bindings and temperature ceilings below are host-specific, which
-- is why they live here rather than in lib/data.lua: that file is the part
-- that ports to another machine, this is the part that does not.

local card = require 'lib.card'
local data = require 'lib.data'

local M = {}

-- chip, file, label, warn, crit.
--
-- Ceilings are per sensor because a single pair cannot serve both: 70C is
-- unremarkable on a CPU package and alarming on an NVMe. Starting values,
-- easy to retune once real numbers under load are known.
local TEMPS = {
  { 'k10temp',      'temp1_input', 'CPU',   75, 90 },
  { 'k10temp',      'temp3_input', 'CCD1',  75, 90 },
  { 'nvme',         'temp1_input', 'NVME',  60, 70 },
  { 'gigabyte_wmi', 'temp2_input', 'BOARD', 60, 70 },
  { 'gigabyte_wmi', 'temp3_input', 'BOARD', 60, 70 },
}

-- One counter per core, created on first draw and kept for the process's life:
-- load is a delta between samples, so the state has to outlive the frame.
local counters = {}
local total_counter = data.new_cpu_counter()

function M.draw(cr, rect, colors)
  local inner = card.card(cr, rect, colors)
  local function clamp(v, lo, hi) return math.max(lo, math.min(hi, v)) end

  local label_size = clamp(inner.w * 0.030, 11, 18)
  local big_size   = clamp(inner.w * 0.085, 26, 56)
  local row_size   = clamp(inner.w * 0.030, 11, 17)

  local stat = data.slurp('/proc/stat')
  local agg_total, agg_idle = data.cpu_times(stat or '')
  local load = total_counter:sample(agg_total, agg_idle)

  local y = inner.y

  -- Aggregate load, the headline number.
  card.font(cr, card.FONT_MONO, label_size, false)
  card.rgba(cr, colors.label)
  card.text(cr, inner.x, y + label_size, 'CPU')

  card.font(cr, card.FONT_HEAVY, big_size, false)
  card.rgba(cr, load and card.threshold(load, 25, 75, colors) or colors.label)
  card.text(cr, inner.x, y + label_size + big_size,
    load and string.format('%d%%', math.floor(load + 0.5)) or '--')

  local ey = y + label_size + big_size + 14

  -- The equaliser: one bar per core, rising from a common baseline.
  local cores = data.per_cpu_times(stat or '')
  local n = #cores
  if n > 0 then
    local gap = math.max(2, inner.w * 0.006)
    local bw = (inner.w - gap * (n - 1)) / n
    local eh = clamp(inner.h * 0.42, 24, 220)
    -- Below about 3px a row of sixteen bars is an illegible smear; drop to the
    -- aggregate bar instead of drawing one.
    if bw >= 3 then
      for i, c in ipairs(cores) do
        counters[i] = counters[i] or data.new_cpu_counter()
        local pct = counters[i]:sample(c.total, c.idle) or 0
        card.vbar(cr, inner.x + (i - 1) * (bw + gap), ey + eh, bw, eh,
          pct / 100, card.threshold(pct, 25, 75, colors), colors)
      end
    else
      card.bar(cr, inner.x, ey + eh - 8, inner.w, 8, (load or 0) / 100,
        card.threshold(load or 0, 25, 75, colors), colors)
    end
    ey = ey + eh + 16
  end

  -- Memory. MemAvailable, not free: it already excludes reclaimable cache, so
  -- it is the figure a person recognises as "used".
  local mem = data.mem_info(data.slurp('/proc/meminfo') or '')
  if mem then
    local frac = mem.used / mem.total
    card.font(cr, card.FONT_MONO, row_size, false)
    card.rgba(cr, colors.label)
    card.text(cr, inner.x, ey, 'RAM')
    card.rgba(cr, colors.value)
    card.text_right(cr, inner.x + inner.w, ey,
      string.format('%s / %s', card.human(mem.used * 1024), card.human(mem.total * 1024)))
    ey = ey + 8
    card.bar(cr, inner.x, ey, inner.w, 8, frac,
      card.threshold(frac * 100, 25, 75, colors), colors)
    ey = ey + 22
  end

  -- Temperatures, each against its own ceiling.
  card.font(cr, card.FONT_MONO, row_size, false)
  local step = row_size * 1.8
  for _, t in ipairs(TEMPS) do
    if ey + step > inner.y + inner.h then break end   -- out of room: stop
    local v = data.sensor(t[1], t[2])
    card.rgba(cr, colors.label)
    card.text(cr, inner.x, ey, t[3])
    card.rgba(cr, v and card.threshold(v, t[4], t[5], colors) or colors.label)
    card.text_right(cr, inner.x + inner.w, ey,
      v and string.format('%d\u{00B0}', v) or '--')
    ey = ey + step
  end
end

return M