aboutsummaryrefslogtreecommitdiffstats
path: root/widgets/system.lua
blob: 1db3c454f6b9d2e9901c070e5080ffbfb84f3056 (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
-- 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 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 load_txt = load and string.format('%d%%', math.floor(load + 0.5)) or '--'
  local cores = data.per_cpu_times(stat or '')
  local temp_n = #TEMPS

  -- Fluid type. One base size S drives the label, headline, RAM and every
  -- temperature row; the equaliser then absorbs the vertical room left over,
  -- so the card fills its cell and S shrinks only where a row would overrun
  -- the width. card.fit_unit supplies the width side of that.
  local LABEL_F, BIG_F, ROW_F = 0.30, 1.0, 0.32
  local groups = {
    { { 'CPU', card.FONT_MONO, LABEL_F }, { load_txt, card.FONT_HEAVY, BIG_F } },
    { { 'RAM', card.FONT_MONO, ROW_F }, { '999.9G / 999.9G', card.FONT_MONO, ROW_F } },
  }
  for _, t in ipairs(TEMPS) do
    groups[#groups + 1] = { { t[3], card.FONT_MONO, ROW_F },
                            { '888\u{00B0}', card.FONT_MONO, ROW_F } }
  end
  -- Height the non-equaliser content needs, in units of S, plus a reserve for
  -- the equaliser band. The smaller of that budget and the width fit is S.
  local fixed_units = 1.00 + 1.13 + temp_n * ROW_F * 1.8 + 0.10
  local S = clamp(math.min(inner.h * 0.96 / (fixed_units + 1.7),
    card.fit_unit(cr, inner.w * 0.96, groups, 100)), 10, 72)
  local label_size = S * LABEL_F
  local big_size   = S * BIG_F
  local row_size   = S * ROW_F

  local y = inner.y

  -- Aggregate load, the headline number. It takes the header line, right
  -- aligned like the big value on every other card, with the label top-left.
  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_right(cr, inner.x + inner.w, y + big_size, load_txt)

  card.font(cr, card.FONT_MONO, label_size, false)
  card.rgba(cr, colors.label)
  card.text(cr, inner.x, y + label_size, 'CPU')

  -- The equaliser: one bar per core, rising from a common baseline. It takes
  -- the room left between the header and the RAM/temperature block, so it is
  -- the card's slack absorber.
  local eq_top = y + big_size + S * 0.30
  local bar_h  = S * 0.18
  local step   = row_size * 1.8
  local eh = clamp((inner.y + inner.h) - eq_top - (S * 1.13 + temp_n * step),
    18, math.huge)
  local n = #cores
  if n > 0 then
    local gap = math.max(2, inner.w * 0.006)
    local bw = (inner.w - gap * (n - 1)) / n
    -- 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), eq_top + eh, bw, eh,
          pct / 100, card.threshold(pct, 25, 75, colors), colors)
      end
    else
      card.bar(cr, inner.x, eq_top + eh - bar_h, inner.w, bar_h, (load or 0) / 100,
        card.threshold(load or 0, 25, 75, colors), colors)
    end
  end
  local ey = eq_top + eh + S * 0.30

  -- 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 + S * 0.30
    card.bar(cr, inner.x, ey, inner.w, bar_h, frac,
      card.threshold(frac * 100, 25, 75, colors), colors)
    ey = ey + bar_h + S * 0.35
  end

  -- Temperatures, each against its own ceiling.
  card.font(cr, card.FONT_MONO, row_size, false)
  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