aboutsummaryrefslogtreecommitdiffstats
path: root/widgets
diff options
context:
space:
mode:
Diffstat (limited to 'widgets')
-rw-r--r--widgets/network.lua173
1 files changed, 173 insertions, 0 deletions
diff --git a/widgets/network.lua b/widgets/network.lua
new file mode 100644
index 0000000..e0ff4cb
--- /dev/null
+++ b/widgets/network.lua
@@ -0,0 +1,173 @@
+-- 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