aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md22
-rw-r--r--conky.conf.in6
-rw-r--r--dashboard.lua17
-rw-r--r--lib/data.lua20
-rw-r--r--test/test_data.lua7
-rw-r--r--widgets/immich.lua3
-rw-r--r--widgets/network.lua16
-rw-r--r--widgets/system.lua23
8 files changed, 87 insertions, 27 deletions
diff --git a/README.md b/README.md
index 59e5797..3de25f7 100644
--- a/README.md
+++ b/README.md
@@ -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