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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
|
-- 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()
-- Sampled every ~2s whatever the draw rate; the values between samples are
-- the last ones read. 1.9 not 2: draws jitter by a few ms either way.
local gate = data.new_gate(1.9)
local load, pcts = nil, {}
-- Read once, not per frame. Neither can change without a reboot, and
-- re-reading /proc/cpuinfo every two seconds for a constant is waste: the file
-- repeats its model-name line once per thread.
--
-- `false` is the cached miss, distinct from nil meaning "not looked up yet",
-- so an absent DMI table is not re-read on every draw.
local cpu_name, board = nil, nil
local function hardware()
if cpu_name == nil then
cpu_name = data.cpu_model(data.slurp('/proc/cpuinfo') or '') or false
end
if board == nil then
local dmi = '/sys/devices/virtual/dmi/id/'
board = data.board_name(data.slurp(dmi .. 'board_vendor'),
data.slurp(dmi .. 'board_name')) or false
end
return cpu_name or nil, board or nil
end
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
if gate() then
local stat = data.slurp('/proc/stat') or ''
load = total_counter:sample(data.cpu_times(stat))
for i, c in ipairs(data.per_cpu_times(stat)) do
counters[i] = counters[i] or data.new_cpu_counter()
pcts[i] = counters[i]:sample(c.total, c.idle) or 0
end
end
local load_txt = load and string.format('%d%%', math.floor(load + 0.5)) or '--'
local temp_n = #TEMPS
local cpu_txt, board_txt = hardware()
-- 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 = {
{ { card.title_str(card.ICON.system, 'CPU'), card.FONT_MONO, LABEL_F },
{ load_txt, card.FONT_HEAVY, BIG_F } },
{ { cpu_txt or '', card.FONT_MONO, ROW_F } },
{ { board_txt or '', card.FONT_MONO, ROW_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.
-- The two identification rows come out of the equaliser's budget: it is the
-- card's designated slack absorber, so it is what gives up the height.
local id_rows = (cpu_txt and 1 or 0) + (board_txt and 1 or 0)
local fixed_units = 1.00 + 1.13 + temp_n * ROW_F * 1.8 + 0.10
+ id_rows * ROW_F * 1.5
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.title(cr, inner.x, y + label_size, card.ICON.system, 'CPU', colors)
-- The hardware this card is about, under the header. Truncated by character,
-- never by byte: a model string can carry a multi-byte character and half of
-- one draws as a replacement box.
-- Two accents rather than one label colour: the rows name two different
-- pieces of hardware, and giving each its own hue separates them at a glance
-- without a prefix like 'CPU:' eating the width. `heading` and `highlight`
-- are the two roles that carry no state meaning, so they can stand for an
-- identity; ok/warning/critical could not, since those say how something is
-- doing and these strings never change.
local id_y = y + big_size + row_size * 0.2
if cpu_txt or board_txt then
card.font(cr, card.FONT_MONO, row_size, false)
local id_limit = math.max(8, math.floor(inner.w / (row_size * 0.55)))
if cpu_txt then
card.rgba(cr, colors.heading)
card.text(cr, inner.x, id_y, card.truncate(cpu_txt, id_limit))
id_y = id_y + row_size * 1.5
end
if board_txt then
card.rgba(cr, colors.highlight)
card.text(cr, inner.x, id_y, card.truncate(board_txt, id_limit))
id_y = id_y + row_size * 1.5
end
end
-- 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.
-- Below the identification rows when there are any, otherwise where it was.
local eq_top = (cpu_txt or board_txt) and (id_y + S * 0.10) or (y + big_size + S * 0.30)
local bar_h = S * 0.18
local step = row_size * 1.8
-- The floor was a flat 18px. With the identification rows above it the
-- equaliser can genuinely run out of room, and a fixed floor means it keeps
-- height it does not have and the temperature rows draw over it. Zero is a
-- legitimate outcome: the guard below skips the band when it has no height.
local eh = clamp((inner.y + inner.h) - eq_top - (S * 1.13 + temp_n * step),
0, math.huge)
local n = #pcts
if n > 0 and eh >= 8 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, pct in ipairs(pcts) do
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
|