aboutsummaryrefslogtreecommitdiffstats
path: root/widgets/network.lua
blob: 3a11521a7a50e3d6ca9e51875e3895f83e27938e (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
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
-- 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, 1.0, 0.30
  local groups = {
    { { card.title_str(card.ICON.network, '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.title(cr, inner.x, inner.y + label_size, card.ICON.network, 'NET', colors)

  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