diff options
| -rw-r--r-- | README.md | 22 | ||||
| -rw-r--r-- | conky.conf.in | 6 | ||||
| -rw-r--r-- | dashboard.lua | 17 | ||||
| -rw-r--r-- | lib/data.lua | 20 | ||||
| -rw-r--r-- | test/test_data.lua | 7 | ||||
| -rw-r--r-- | widgets/immich.lua | 3 | ||||
| -rw-r--r-- | widgets/network.lua | 16 | ||||
| -rw-r--r-- | widgets/system.lua | 23 |
8 files changed, 87 insertions, 27 deletions
@@ -45,7 +45,7 @@ the file. Until it works, and whenever the cache is missing, the card draws its own chrome with `no weather data` and the thing to fix, rather than vanishing. Fetching is scheduled by conky itself: `conky.text` carries a single -`${execi 900 ~/.config/conky/bin/weather-fetch.sh}`, so the cache refreshes +`${texeci 900 ~/.config/conky/bin/weather-fetch.sh}`, so the cache refreshes every 15 minutes while the dashboard runs and never while it does not. The widget only ever reads `~/.cache/udt/weather.json`; it makes no network call and cannot block the draw. @@ -85,16 +85,16 @@ The disks and cache cards read a cache file that a sampler writes; they never call `du` or `statfs` themselves. `statfs` on an unreachable NFS path blocks, and the disks sampler exists so that a dead server costs a stale cache rather than a frozen draw. Sampling is scheduled by conky itself: `conky.text` carries -an `${execi}` per sampler, even though that block renders nothing under Cairo. - - ${execi 900 ~/.config/conky/bin/weather-fetch.sh} - ${execi 60 ~/.config/conky/bin/disks-sample.sh} - ${execi 900 ~/.config/conky/bin/cache-sample.sh} - ${execi 1800 ~/.config/conky/bin/pubip-sample.sh} - ${execi 900 ~/.config/conky/bin/slackware-sample.sh} - ${execi 300 ~/.config/conky/bin/calendar-sample.sh} - ${execi 1800 ~/.config/conky/bin/immich-fetch.sh} - ${execi 600 ~/.config/conky/bin/updates-sample.sh} +a threaded `${texeci}` per sampler, even though that block renders nothing under Cairo. + + ${texeci 900 ~/.config/conky/bin/weather-fetch.sh} + ${texeci 60 ~/.config/conky/bin/disks-sample.sh} + ${texeci 900 ~/.config/conky/bin/cache-sample.sh} + ${texeci 1800 ~/.config/conky/bin/pubip-sample.sh} + ${texeci 900 ~/.config/conky/bin/slackware-sample.sh} + ${texeci 300 ~/.config/conky/bin/calendar-sample.sh} + ${texeci 1800 ~/.config/conky/bin/immich-fetch.sh} + ${texeci 600 ~/.config/conky/bin/updates-sample.sh} The disks sampler runs once a minute; the cache, Slackware and public-IP samplers run every 15, 15 and 30 minutes respectively, because `du -sh diff --git a/conky.conf.in b/conky.conf.in index a0b354e..d12ffc1 100644 --- a/conky.conf.in +++ b/conky.conf.in @@ -56,4 +56,8 @@ conky.config = { -- samplers, and tying it to conky means nothing fetches while the dashboard is -- down. breaktimer runs at 5s, its daemon's own tick, so the countdown advances -- in the steps the daemon counts in. -conky.text = [[${execi 900 ~/.config/conky/bin/weather-fetch.sh}${execi 60 ~/.config/conky/bin/disks-sample.sh}${execi 900 ~/.config/conky/bin/cache-sample.sh}${execi 1800 ~/.config/conky/bin/pubip-sample.sh}${execi 900 ~/.config/conky/bin/slackware-sample.sh}${execi 300 ~/.config/conky/bin/calendar-sample.sh}${execi 5 ~/.config/conky/bin/breaktimer-sample.sh}${execi 1800 ~/.config/conky/bin/immich-fetch.sh}${execi 600 ~/.config/conky/bin/updates-sample.sh}]] +-- +-- texeci, not execi: execi runs in conky's main loop and blocks the draw until +-- the script exits. At 0.1s while visible, a 0.7s nvtable or an Immich +-- download stalls the scroll; texeci runs each in its own thread. +conky.text = [[${texeci 900 ~/.config/conky/bin/weather-fetch.sh}${texeci 60 ~/.config/conky/bin/disks-sample.sh}${texeci 900 ~/.config/conky/bin/cache-sample.sh}${texeci 1800 ~/.config/conky/bin/pubip-sample.sh}${texeci 900 ~/.config/conky/bin/slackware-sample.sh}${texeci 300 ~/.config/conky/bin/calendar-sample.sh}${texeci 5 ~/.config/conky/bin/breaktimer-sample.sh}${texeci 1800 ~/.config/conky/bin/immich-fetch.sh}${texeci 600 ~/.config/conky/bin/updates-sample.sh}]] diff --git a/dashboard.lua b/dashboard.lua index 18ddf80..1db0b6e 100644 --- a/dashboard.lua +++ b/dashboard.lua @@ -168,6 +168,17 @@ local function draw(cr, sw, sh, colors) end end +-- Draw rate follows visibility. The dashboard lives on a special workspace +-- and is hidden most of the time, so it idles at the config's update_interval +-- (2s) and draws at VISIBLE_INTERVAL while shown, which is what makes the wheel +-- scroll live: conky has no way for Lua to request a single redraw. +-- +-- Shown and hidden are the pointer entering and leaving the window, which +-- Hyprland sends on every toggle of the workspace. The pointer is on the +-- focused monitor, where the special workspace opens, so enter follows show. +local VISIBLE_INTERVAL = 0.1 +local hidden_interval = nil + -- Pointer events, handed to the widget whose cell is under the pointer, if it -- defines mouse(event, rect). Coordinates are window-relative, and the window -- is the whole surface, so they index the same grid the draw uses. @@ -175,6 +186,12 @@ end -- Wrapped like the draw: a fault here must not take conky down. Returning true -- tells conky the event was consumed. function conky_mouse(event) + if event.type == 'mouse_enter' or event.type == 'mouse_leave' then + hidden_interval = hidden_interval or conky_info.update_interval + conky_set_update_interval(event.type == 'mouse_enter' + and VISIBLE_INTERVAL or hidden_interval) + return false + end if conky_window == nil then return false end local sw, sh = conky_window.width, conky_window.height for _, entry in ipairs(layout) do diff --git a/lib/data.lua b/lib/data.lua index 4b31f43..7ae96a7 100644 --- a/lib/data.lua +++ b/lib/data.lua @@ -631,6 +631,26 @@ function M.immich_parse(text) return out end +-- A sampling gate for counters that are read per draw. The dashboard draws +-- every 0.1s while it is on screen, and a CPU or byte counter sampled that +-- often gives coarse, flickering deltas, so those cards sample on this gate +-- and redraw their last values in between. +-- +-- The returned function answers true at most once per `every` seconds, plus +-- the clock it used: /proc/uptime, because os.time() counts whole seconds and +-- a rate divided by a rounded interval is off by up to half. `now` is for the +-- test. An unreadable clock opens the gate, which is the old per-draw behaviour. +function M.new_gate(every) + local last + return function(now) + now = now or tonumber((M.slurp('/proc/uptime') or ''):match('^(%S+)')) + if not now then return true, nil end + if last and now - last < every then return false, now end + last = now + return true, now + end +end + -- nvtable's plain table (`nvtable -C -E`), as bin/updates-sample.sh caches it: -- -- [personal] foo-bin 1.14.9 -> 1.14.9 !! ~ 1.14.10 diff --git a/test/test_data.lua b/test/test_data.lua index ec95fae..2d4d5c7 100644 --- a/test/test_data.lua +++ b/test/test_data.lua @@ -478,3 +478,10 @@ assert(not up[6].behind and not up[6].bump, 'updates row 6: current') assert(up.upgrade == 3 and up.bump == 1 and up.current == 46 and up.missing == 18, 'updates summary') assert(data.nvtable_parse('') == nil, 'updates: empty cache is nil') + +-- new_gate: opens at most once per interval on the clock it is given. +local gate = data.new_gate(1.9) +assert(gate(100.0) == true, 'gate: first call opens') +assert(gate(100.5) == false, 'gate: closed inside the interval') +assert(gate(101.95) == true, 'gate: opens after it') +assert(gate(102.0) == false, 'gate: interval restarts from the last open') diff --git a/widgets/immich.lua b/widgets/immich.lua index 8590dbf..d700d8a 100644 --- a/widgets/immich.lua +++ b/widgets/immich.lua @@ -159,9 +159,6 @@ end -- Scroll steps through today's photos; a left click opens the one on screen -- in Immich and hides the dashboard, since the browser would otherwise open -- underneath the special workspace. --- --- ponytail: a scroll shows at the next draw, up to update_interval (2s) later. --- Conky offers Lua no way to request a redraw. function M.mouse(event) if event.type == 'mouse_scroll' then offset = offset + (event.direction == 'up' and -1 or 1) diff --git a/widgets/network.lua b/widgets/network.lua index 3a11521..20b0573 100644 --- a/widgets/network.lua +++ b/widgets/network.lua @@ -37,6 +37,11 @@ local rx_hist, tx_hist = {}, {} local hist_len = 0 local lan_addr = nil +-- Sampled every ~2s whatever the draw rate, so the chart keeps one point per +-- sample rather than per draw. 1.9 not 2: draws jitter by a few ms. +local gate = data.new_gate(1.9) +local down, up = nil, 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 @@ -73,8 +78,11 @@ function M.draw(cr, rect, colors) 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')) + local sampled, now = gate() + if sampled then + down = rx_counter:sample(read_counter('rx_bytes'), now) + up = tx_counter:sample(read_counter('tx_bytes'), now) + end -- 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. @@ -140,8 +148,8 @@ function M.draw(cr, rect, colors) 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 + if sampled and down then push(rx_hist, down, hist_len) end + if sampled and 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. diff --git a/widgets/system.lua b/widgets/system.lua index 98e0ae1..c1cdf67 100644 --- a/widgets/system.lua +++ b/widgets/system.lua @@ -27,6 +27,11 @@ local TEMPS = { 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. @@ -51,11 +56,15 @@ 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) + 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 cores = data.per_cpu_times(stat or '') local temp_n = #TEMPS local cpu_txt, board_txt = hardware() @@ -137,16 +146,14 @@ function M.draw(cr, rect, colors) -- 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 = #cores + 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, 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 + 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 |
