-- Network: throughput as two lines on one chart, with the LAN and public -- addresses. -- -- The rates come from the interface's own byte counters, read every draw: two -- file reads, no subprocess. The public address comes from a cache file, -- because a network call in the draw hook would freeze the dashboard when it -- hung. local card = require 'lib.card' local data = require 'lib.data' local M = {} -- The interface to graph. br0 is this host's bridge, matching the old text -- config. -- -- Note what this means: a bridge carries VM-to-host traffic that never reaches -- the router, so a local copy spikes the graph. That was true of the old -- config too and is accepted; this constant is the one edit that changes it. local IFACE = 'br0' local CACHE = (os.getenv('XDG_CACHE_HOME') or (os.getenv('HOME') .. '/.cache')) .. '/udt/pubip.txt' -- A public address older than this is not shown. Eight sampling intervals: by -- then the sampler has failed repeatedly, and an address that may no longer be -- yours displayed with confidence is worse than '--'. local STALE_AFTER = 4 * 3600 local STAT = '/sys/class/net/' .. IFACE .. '/statistics/' -- Module state: the history outlives the frame, which is the whole point. -- It does not outlive a conky restart, so the chart starts empty and fills -- left to right. local rx_counter, tx_counter local rx_hist, tx_hist = {}, {} local hist_len = 0 local lan_addr = nil -- Append to a fixed-length history, dropping the oldest. A plain array with -- table.remove(1) rather than a circular buffer with an index: the lengths -- here are a few hundred at most and the draw is every two seconds, so the -- O(n) shift is free and the array is already in draw order. local function push(hist, v, len) hist[#hist + 1] = v while #hist > len do table.remove(hist, 1) end end local function read_counter(file) local s = data.slurp(STAT .. file) if not s then return nil end return tonumber(s:match('^%s*(%d+)')) end -- Bytes per second to a short string. Deliberately not card.human: a rate -- wants a '/s' and one decimal at most, and reusing card.human would put a -- suffix meant for capacity onto a speed. local function rate_str(bps) if not bps then return '--' end local units = { 'B', 'K', 'M', 'G' } local n, i = bps, 1 while n >= 1024 and i < #units do n = n / 1024; i = i + 1 end if i == 1 then return string.format('%d%s/s', math.floor(n), units[i]) end if n >= 100 then return string.format('%.0f%s/s', n, units[i]) end return string.format('%.1f%s/s', n, units[i]) 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 local interval = (conky_info and conky_info.update_interval) or 2 rx_counter = rx_counter or data.new_rate_counter(interval) tx_counter = tx_counter or data.new_rate_counter(interval) local down = rx_counter:sample(read_counter('rx_bytes')) local up = tx_counter:sample(read_counter('tx_bytes')) -- Fluid type, as every other card does: one base size S drives everything, -- taken as the smaller of a height budget and a measured width fit. local LABEL_F, BIG_F, ROW_F = 0.30, 0.62, 0.30 local groups = { { { 'NET', card.FONT_MONO, LABEL_F }, { '888.8M/s', card.FONT_HEAVY, BIG_F } }, { { '\u{F0318} 192.000.000.000', card.FONT_MONO, ROW_F } }, { { '\u{F01DA} 888.8M/s', card.FONT_MONO, ROW_F }, { '\u{F0552} 888.8M/s', card.FONT_MONO, ROW_F } }, } local fixed_units = 1.0 + ROW_F * 2.2 * 3 local S = clamp(math.min(inner.h * 0.96 / (fixed_units + 1.2), card.fit_unit(cr, inner.w * 0.96, groups, 100)), 10, 72) local label_size = S * LABEL_F local row_size = S * ROW_F -- Header: the label left, the download rate as the big value at the right. -- Download is the headline because it is the number that moves. card.font(cr, card.FONT_HEAVY, S * BIG_F, false) card.rgba(cr, colors.body) card.text_right(cr, inner.x + inner.w, inner.y + S * BIG_F, rate_str(down)) card.font(cr, card.FONT_MONO, label_size, false) card.rgba(cr, colors.label) card.text(cr, inner.x, inner.y + label_size, 'NET') local ey = inner.y + S * BIG_F + row_size * 1.4 local step = row_size * 2.2 -- The addresses. LAN is memoized on first success and retried while nil: a -- bridge may not be up when conky starts. lan_addr = lan_addr or data.iface_addr(IFACE) card.font(cr, card.FONT_MONO, row_size, false) card.rgba(cr, colors.label) card.text(cr, inner.x, ey, '\u{F0318}') -- LAN card.rgba(cr, colors.value) card.text_right(cr, inner.x + inner.w, ey, lan_addr or '--') ey = ey + step local pub = data.kv_parse(data.slurp(CACHE) or '') local fetched = tonumber(pub.fetched) local pub_txt = '--' if pub.ip and fetched and (os.time() - fetched) < STALE_AFTER then pub_txt = pub.ip end card.rgba(cr, colors.label) card.text(cr, inner.x, ey, '\u{F059F}') -- globe card.rgba(cr, pub_txt == '--' and colors.label or colors.value) card.text_right(cr, inner.x + inner.w, ey, pub_txt) ey = ey + step * 1.1 -- The chart takes the room left between the addresses and the rate row. local rate_row_h = row_size * 2.4 local chart_top = ey local chart_h = (inner.y + inner.h) - chart_top - rate_row_h if chart_h > 12 then -- One sample per pixel column, so the chart never interpolates. The -- buffer is resized when the card is, keeping what it can: a moved card -- should not clear the history. local want = math.max(8, math.floor(inner.w)) if want ~= hist_len then hist_len = want while #rx_hist > hist_len do table.remove(rx_hist, 1) end while #tx_hist > hist_len do table.remove(tx_hist, 1) end end if down then push(rx_hist, down, hist_len) end if up then push(tx_hist, up, hist_len) end -- One ceiling for both series. Scaled independently they would lie about -- their relative size, which is the entire reason to draw them together. local peak = 0 for _, v in ipairs(rx_hist) do if v > peak then peak = v end end for _, v in ipairs(tx_hist) do if v > peak then peak = v end end card.plot(cr, inner.x, chart_top, inner.w, chart_h, { { values = rx_hist, colour = colors.ok }, { values = tx_hist, colour = colors.highlight }, }, peak, colors) -- The peak, so a full-height line means something in absolute terms. card.font(cr, card.FONT_MONO, row_size * 0.85, false) card.rgba(cr, colors.label) card.text(cr, inner.x, chart_top + row_size * 0.85, rate_str(peak)) end -- The live rates, each in its series colour so the line and the number are -- unmistakably the same thing. local ry = inner.y + inner.h - row_size * 0.4 card.font(cr, card.FONT_MONO, row_size, false) card.rgba(cr, colors.ok) card.text(cr, inner.x, ry, '\u{F01DA} ' .. rate_str(down)) card.rgba(cr, colors.highlight) card.text_right(cr, inner.x + inner.w, ry, '\u{F0552} ' .. rate_str(up)) end return M