From 3943e08dabe684b409a3ac99c9371fbeb5df5cdb Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 13 Oct 2026 11:35:00 +0200 Subject: feat: system, gpu, disks and network move to the quickshell island The quickshell island's performance panel now shows CPU, RAM, temperatures, the GPU, disks and network, and warns from the status pill when something runs hot, fills up, or the public IP drifts from the pinned one. Two dashboards drawing the same numbers would drift apart, so the four widgets go, with the disks and public IP samplers. The island keeps writing ~/.cache/udt/pubip.txt in the same format. The freed space is left empty for the user to rearrange. Co-Authored-By: Claude Sonnet 5.5 --- test/fixtures/df_output | 7 -- test/fixtures/ip_addr | 3 - test/fixtures/proc_cpuinfo | 12 -- test/fixtures/proc_cpuinfo_intel | 6 - test/fixtures/proc_meminfo | 7 -- test/fixtures/proc_stat | 5 - test/fixtures/proc_stat_percore | 11 -- test/fixtures/temp1_input | 1 - test/test_data.lua | 263 --------------------------------------- 9 files changed, 315 deletions(-) delete mode 100644 test/fixtures/df_output delete mode 100644 test/fixtures/ip_addr delete mode 100644 test/fixtures/proc_cpuinfo delete mode 100644 test/fixtures/proc_cpuinfo_intel delete mode 100644 test/fixtures/proc_meminfo delete mode 100644 test/fixtures/proc_stat delete mode 100644 test/fixtures/proc_stat_percore delete mode 100644 test/fixtures/temp1_input (limited to 'test') diff --git a/test/fixtures/df_output b/test/fixtures/df_output deleted file mode 100644 index 9205c83..0000000 --- a/test/fixtures/df_output +++ /dev/null @@ -1,7 +0,0 @@ -Filesystem 1-blocks Used Available Capacity Mounted on -/dev/mapper/myvg-root 263174213632 208111570944 41746907136 84% / -/dev/mapper/myvg-home 719407267840 219043332096 463804289024 33% /home -/dev/sda1 983350091776 547869650944 385453473792 59% /data -server-a:/Volume1/Library 3958241259520 2966853025792 815851831296 79% /mnt/nfs/Library -server-b:/mnt/HD/HD_a2/share 982889670656 480039763968 503676862464 49% /mnt/nfs/shared -/dev/full 100000000000 100000000000 0 100% /mnt/full diff --git a/test/fixtures/ip_addr b/test/fixtures/ip_addr deleted file mode 100644 index 8d3f212..0000000 --- a/test/fixtures/ip_addr +++ /dev/null @@ -1,3 +0,0 @@ -4: br0: mtu 1500 qdisc noqueue state UP group default qlen 1000 - inet 192.0.2.15/24 brd 192.0.2.255 scope global br0 - valid_lft forever preferred_lft forever diff --git a/test/fixtures/proc_cpuinfo b/test/fixtures/proc_cpuinfo deleted file mode 100644 index e9cd523..0000000 --- a/test/fixtures/proc_cpuinfo +++ /dev/null @@ -1,12 +0,0 @@ -processor : 0 -vendor_id : AuthenticAMD -cpu family : 26 -model : 68 -model name : AMD Ryzen 7 9700X 8-Core Processor -stepping : 0 -cpu MHz : 4491.436 -cache size : 1024 KB - -processor : 1 -vendor_id : AuthenticAMD -model name : AMD Ryzen 7 9700X 8-Core Processor diff --git a/test/fixtures/proc_cpuinfo_intel b/test/fixtures/proc_cpuinfo_intel deleted file mode 100644 index 886b589..0000000 --- a/test/fixtures/proc_cpuinfo_intel +++ /dev/null @@ -1,6 +0,0 @@ -processor : 0 -vendor_id : GenuineIntel -cpu family : 6 -model : 158 -model name : Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz -stepping : 10 diff --git a/test/fixtures/proc_meminfo b/test/fixtures/proc_meminfo deleted file mode 100644 index 9c279aa..0000000 --- a/test/fixtures/proc_meminfo +++ /dev/null @@ -1,7 +0,0 @@ -MemTotal: 31943076 kB -MemFree: 4291244 kB -MemAvailable: 25627088 kB -Buffers: 123456 kB -Cached: 6789012 kB -SwapTotal: 8388604 kB -SwapFree: 8388604 kB diff --git a/test/fixtures/proc_stat b/test/fixtures/proc_stat deleted file mode 100644 index 2914cca..0000000 --- a/test/fixtures/proc_stat +++ /dev/null @@ -1,5 +0,0 @@ -cpu 209094 2199 135469 11604898 24408 0 946 0 0 0 -cpu0 5294 11 2928 740049 460 0 592 0 0 0 -cpu1 5210 18 2801 740512 431 0 12 0 0 0 -intr 12345678 -ctxt 987654321 diff --git a/test/fixtures/proc_stat_percore b/test/fixtures/proc_stat_percore deleted file mode 100644 index cbf62f2..0000000 --- a/test/fixtures/proc_stat_percore +++ /dev/null @@ -1,11 +0,0 @@ -cpu 293470 5201 186259 16513195 82679 0 2095 0 0 0 -cpu0 6441 24 3225 1056496 1932 0 760 0 0 0 -cpu1 23587 123 15762 1018523 8299 0 112 0 0 0 -cpu2 9859 24 6098 1049850 2386 0 81 0 0 0 -cpu3 11200 30 7000 1040000 2500 0 90 0 0 0 -intr 123456789 0 0 0 -ctxt 987654321 -btime 1789600000 -processes 54321 -procs_running 2 -procs_blocked 0 diff --git a/test/fixtures/temp1_input b/test/fixtures/temp1_input deleted file mode 100644 index 46fba94..0000000 --- a/test/fixtures/temp1_input +++ /dev/null @@ -1 +0,0 @@ -53125 diff --git a/test/test_data.lua b/test/test_data.lua index 549ce40..37a302e 100644 --- a/test/test_data.lua +++ b/test/test_data.lua @@ -20,141 +20,7 @@ local function read(path) return s end --- cpu_times: total and idle jiffies from the aggregate "cpu " line. -local total, idle = data.cpu_times(read('test/fixtures/proc_stat')) --- 209094+2199+135469+11604898+24408+0+946 = 11977014 -assert(total == 11977014, 'cpu total, got ' .. tostring(total)) --- idle field is the 4th value, 11604898; iowait (24408) counts as idle too -assert(idle == 11629306, 'cpu idle, got ' .. tostring(idle)) - --- mem_info: values in kB, as the file gives them. -local mem = data.mem_info(read('test/fixtures/proc_meminfo')) -assert(mem.total == 31943076, 'mem total, got ' .. tostring(mem.total)) -assert(mem.available == 25627088, 'mem available, got ' .. tostring(mem.available)) --- used is total minus available, which is what a user means by "used" -assert(mem.used == 6315988, 'mem used, got ' .. tostring(mem.used)) - --- millidegrees: hwmon temp*_input is millidegrees C, rounded to whole degrees. -assert(data.millidegrees(read('test/fixtures/temp1_input')) == 53, - 'temp, got ' .. tostring(data.millidegrees(read('test/fixtures/temp1_input')))) --- A missing or unreadable sensor must yield nil, not an error and not 0: --- 0 degrees is a plausible reading and would be indistinguishable from failure. -assert(data.millidegrees(nil) == nil, 'nil input must give nil') -assert(data.millidegrees('') == nil, 'empty input must give nil') -assert(data.millidegrees('garbage') == nil, 'unparseable input must give nil') - --- CPU load is a delta between two samples, so the counter holds state. --- The first call has no previous sample and must report nil, not a number: --- any number it invented would be wrong, and 100% on startup looks like a --- real spike. -local c = data.new_cpu_counter() -assert(c:sample(1000, 900) == nil, 'first sample must give nil') - --- Second sample: 100 more total jiffies, 50 of them idle, so 50% busy. --- Held in a local first: calling sample() again inside the assert message would --- advance the counter a third time. -local busy = c:sample(1100, 950) -assert(busy == 50.0, 'second sample, got ' .. tostring(busy)) - --- A counter that did not advance means no elapsed time, not 0% load. -local c2 = data.new_cpu_counter() -c2:sample(1000, 900) -assert(c2:sample(1000, 900) == nil, 'zero delta must give nil, not a division by zero') - --- === Per-core CPU ========================================================= --- The aggregate line answers "how busy is the machine"; the equaliser needs --- one entry per core. Both come from the same file, so they are parsed by the --- same rules and differ only in which lines they read. -local percore = read('test/fixtures/proc_stat_percore') -local cores = data.per_cpu_times(percore) -assert(#cores == 4, 'one entry per cpuN line, got ' .. tostring(#cores)) - --- cpu0: 6441+24+3225+1056496+1932+0+760 = 1068878 total, idle+iowait = 1058428 -assert(cores[1].total == 1068878, 'core 0 total, got ' .. tostring(cores[1].total)) -assert(cores[1].idle == 1058428, 'core 0 idle, got ' .. tostring(cores[1].idle)) - --- Ordering matters: bar N must be core N, so the list follows the file. -assert(cores[2].total == 1066406, 'core 1 total, got ' .. tostring(cores[2].total)) - --- The aggregate 'cpu ' line must NOT be counted as a core: it would draw a --- bar for the aggregate line showing the average, which looks like a real core. -for i, c in ipairs(cores) do - assert(c.total < 2000000, 'entry ' .. i .. ' looks like the aggregate line') -end - --- A counter per core is just another instance, which is why new_cpu_counter --- holds its own previous sample rather than using a module-level one. -local c0 = data.new_cpu_counter() -assert(c0:sample(cores[1].total, cores[1].idle) == nil, 'first sample is nil') -local busy = c0:sample(cores[1].total + 100, cores[1].idle + 25) -assert(busy == 75.0, 'second sample 75%, got ' .. tostring(busy)) - --- Malformed input yields an empty list, never an error: a raise here is a --- blank dashboard. -assert(#data.per_cpu_times('') == 0, 'empty input gives an empty list') -assert(#data.per_cpu_times('garbage\nlines\n') == 0, 'garbage gives an empty list') -assert(#data.per_cpu_times(nil) == 0, 'nil gives an empty list') --- A truncated line (fewer than the 5 fields the maths needs) is skipped --- rather than producing a nonsense total. -assert(#data.per_cpu_times('cpu0 1 2\n') == 0, 'a short line is skipped') - -- === Cache files ========================================================== --- df -P guarantees one line per mount with the mountpoint LAST, which is why --- the parser takes the last field rather than the sixth: an NFS device is --- 'server:/export' and a device name can carry surprises, but the mountpoint --- is always at the end. -local fs = data.df_parse(read('test/fixtures/df_output')) -assert(#fs == 6, 'one entry per mount, got ' .. tostring(#fs)) -assert(fs[1].mount == '/', 'first mount, got ' .. tostring(fs[1].mount)) -assert(fs[1].pct == 84, 'root percent, got ' .. tostring(fs[1].pct)) -assert(fs[1].size == 263174213632, 'root size in bytes, got ' .. tostring(fs[1].size)) -assert(fs[1].used == 208111570944, 'root used, got ' .. tostring(fs[1].used)) - --- Available is READ, never derived as size - used: the two differ by the --- root-reserved blocks, about 5%, which on this root is some 13GB that exists --- but cannot be used. A card claiming that space is free would be lying. -assert(fs[1].avail == 41746907136, 'root available, got ' .. tostring(fs[1].avail)) -assert(fs[1].size - fs[1].used ~= fs[1].avail, - 'the fixture must exercise the reserved-block gap, or this assertion proves nothing') - --- fs[3] is /dev/sda1: a device name ENDING IN A DIGIT, next to a numeric --- column. A parser counting fields from the left, or matching digits without --- anchoring, reads the partition number as a size. -assert(fs[3].mount == '/data', 'sda1 mount, got ' .. tostring(fs[3].mount)) -assert(fs[3].dev == '/dev/sda1', 'sda1 device, got ' .. tostring(fs[3].dev)) -assert(fs[3].size == 983350091776, 'sda1 size, got ' .. tostring(fs[3].size)) -assert(fs[3].used == 547869650944, 'sda1 used, got ' .. tostring(fs[3].used)) -assert(fs[3].avail == 385453473792, 'sda1 available, got ' .. tostring(fs[3].avail)) -assert(fs[3].pct == 59, 'sda1 percent, got ' .. tostring(fs[3].pct)) -assert(fs[3].host == nil, 'a local device has no host') - --- A full filesystem reports zero available, which is a real reading. -assert(fs[6].avail == 0, 'a full mount has 0 available, got ' .. tostring(fs[6].avail)) - --- The NFS rows are the reason the last field matters: a colon in the device --- would break a parser splitting on punctuation. -assert(fs[4].mount == '/mnt/nfs/Library', 'nfs mount, got ' .. tostring(fs[4].mount)) -assert(fs[4].pct == 79, 'nfs percent, got ' .. tostring(fs[4].pct)) - --- The device field carries the server, which is what lets the disks card --- label the two shares without an address hardcoded in the repo. -assert(fs[4].host == 'server-a', 'nfs host, got ' .. tostring(fs[4].host)) -assert(fs[1].host == nil, 'a local device has no host, got ' .. tostring(fs[1].host)) - --- A full filesystem is a real state, not an error. -assert(fs[6].pct == 100, 'a full mount reads 100, got ' .. tostring(fs[6].pct)) - --- The header line must not become a row. -for _, e in ipairs(fs) do - assert(e.mount ~= 'on' and e.mount ~= 'Mounted', 'the header leaked in as a row') -end - -assert(#data.df_parse('') == 0, 'empty input gives an empty list') -assert(#data.df_parse(nil) == 0, 'nil gives an empty list') --- A header with no rows is what a failed df produces. -assert(#data.df_parse('Filesystem 1-blocks Used Available Capacity Mounted on\n') == 0, - 'a header alone gives an empty list') - -- du -sh: total first, then children largest-first. The suffixes must be -- converted, not string-sorted: '1.1G' outranks '245M' numerically and loses -- to it alphabetically. @@ -199,46 +65,6 @@ assert(cut == string.rep('\u{00E9}', 9) .. '\u{2026}', assert(card.truncate(nil, 10) == '', 'nil truncates to empty') assert(card.truncate('abc', 0) == '', 'a zero limit gives empty') --- === Network rate ========================================================= --- Interface byte counters are cumulative, so a rate is a delta over elapsed --- time. The first call has no previous sample and must report nil: any number --- it invented would be wrong, and a spike at startup looks real. -local r = data.new_rate_counter() -assert(r:sample(1000, 100) == nil, 'first sample must give nil') - --- 2048 bytes over 2 seconds is 1024 B/s. -local rate = r:sample(3048, 102) -assert(rate == 1024, 'second sample, got ' .. tostring(rate)) - --- A counter that went backwards means a wrap or an interface reset. It must --- clamp to zero, never produce the huge positive an unsigned wrap implies: --- one bogus sample poisons the shared autoscale for the whole window. -local r2 = data.new_rate_counter() -r2:sample(5000, 100) -assert(r2:sample(10, 102) == 0, 'a counter reset must give 0') - --- Two samples inside the same clock second. os.time() has whole-second --- resolution against a 2s draw interval, so this happens in normal operation --- and must not divide by zero. -local r3 = data.new_rate_counter() -r3:sample(1000, 500) -local same_second = r3:sample(2000, 500) -assert(same_second ~= nil and same_second >= 0, - 'a zero time delta must fall back to the draw interval, got ' .. tostring(same_second)) - --- A missing interface reads nil, which must propagate rather than raise. -local r4 = data.new_rate_counter() -assert(r4:sample(nil, 100) == nil, 'a nil byte count gives nil') - --- The fallback interval is a constructor argument, and the widget passes --- conky's real update_interval rather than letting it default. That is the --- path that ships, so it is the path that gets a test: at a 5s interval the --- same 2048 bytes must read as 409.6 B/s, not 1024. -local r5 = data.new_rate_counter(5) -r5:sample(1000, 100) -assert(r5:sample(3048, 100) == 2048 / 5, - 'an explicit fallback interval must be used, got ' .. tostring(r5:sample(3048, 100))) - -- === kv_parse ============================================================= -- The slackware sampler writes 'key value' lines. The format is deliberately -- dumber than the data: epochs stay integers rather than becoming formatted @@ -260,68 +86,6 @@ assert(type(data.kv_parse('')) == 'table', 'empty input gives a table') assert(type(data.kv_parse(nil)) == 'table', 'nil gives a table') assert(data.kv_parse('keyonly\n').keyonly == nil, 'a line with no value is skipped') --- === Hardware identification ============================================== --- Both strings are constant for the machine's life, so the widget memoizes --- them. What is tested here is the stripping, which must be a rule rather --- than a special case for one host: the Intel fixture exists to prove it. -assert(data.cpu_model(read('test/fixtures/proc_cpuinfo')) == 'Ryzen 7 9700X', - 'amd cpu, got ' .. tostring(data.cpu_model(read('test/fixtures/proc_cpuinfo')))) -assert(data.cpu_model(read('test/fixtures/proc_cpuinfo_intel')) == 'Core i7-8700K', - 'intel cpu, got ' .. tostring(data.cpu_model(read('test/fixtures/proc_cpuinfo_intel')))) - --- Unparseable input yields nil, so the card shows nothing rather than a --- half-stripped string. -assert(data.cpu_model('') == nil, 'empty cpuinfo gives nil') -assert(data.cpu_model(nil) == nil, 'nil cpuinfo gives nil') -assert(data.cpu_model('processor\t: 0\n') == nil, 'cpuinfo with no model name gives nil') - --- The board is two sysfs files joined, with the vendor's corporate suffix --- dropped: it is boilerplate on every board and costs a third of the row. -assert(data.board_name('Gigabyte Technology Co., Ltd.\n', 'X870 EAGLE WIFI7\n') - == 'Gigabyte X870 EAGLE WIFI7', - 'board, got ' .. tostring(data.board_name('Gigabyte Technology Co., Ltd.\n', 'X870 EAGLE WIFI7\n'))) -assert(data.board_name('ASUSTeK COMPUTER INC.\n', 'PRIME B650-PLUS\n') - == 'ASUSTeK PRIME B650-PLUS', - 'asus board, got ' .. tostring(data.board_name('ASUSTeK COMPUTER INC.\n', 'PRIME B650-PLUS\n'))) - --- A machine that reports one and not the other shows what it has. -assert(data.board_name(nil, 'X870 EAGLE WIFI7\n') == 'X870 EAGLE WIFI7', - 'name alone, got ' .. tostring(data.board_name(nil, 'X870 EAGLE WIFI7\n'))) -assert(data.board_name('Gigabyte\n', nil) == 'Gigabyte', 'vendor alone') -assert(data.board_name(nil, nil) == nil, 'neither gives nil') - --- A virtual machine reports placeholder DMI strings. Showing 'To be filled by --- O.E.M.' as the motherboard is worse than showing nothing. -assert(data.board_name('To Be Filled By O.E.M.\n', 'To Be Filled By O.E.M.\n') == nil, - 'placeholder DMI gives nil') - --- Three Intel forms the single fixture does not cover. The first fixture --- happens to carry a literal 'CPU' before the clock, so a pattern requiring --- that token passes the fixture and still leaves '@ 2.40GHz' on the card for --- every other Intel part. These exist so the stripping stays a rule. -assert(data.cpu_model('model name\t: Intel(R) Xeon(R) CPU E5-2680 v4 @ 2.40GHz\n') - == 'Xeon E5-2680 v4', - 'xeon, got ' .. tostring(data.cpu_model('model name\t: Intel(R) Xeon(R) CPU E5-2680 v4 @ 2.40GHz\n'))) -assert(data.cpu_model('model name\t: 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz\n') - == 'Core i7-1165G7', - 'tiger lake, got ' .. tostring(data.cpu_model('model name\t: 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz\n'))) -assert(data.cpu_model('model name\t: AMD Ryzen 9 7950X3D 16-Core Processor\n') - == 'Ryzen 9 7950X3D', - 'x3d, got ' .. tostring(data.cpu_model('model name\t: AMD Ryzen 9 7950X3D 16-Core Processor\n'))) - --- === Interface address ==================================================== --- The fixture uses TEST-NET-1 (192.0.2.0/24, RFC 5737). This repository is --- public: no real LAN address belongs in it, and no test may assert one. -local addr = data.iface_addr_parse(read('test/fixtures/ip_addr')) -assert(addr == '192.0.2.15', 'lan address, got ' .. tostring(addr)) - --- A down interface has no inet line. nil, so the card shows '--' rather than --- a stale or invented address. -assert(data.iface_addr_parse('5: br0: mtu 1500 state DOWN\n') == nil, - 'a down interface gives nil') -assert(data.iface_addr_parse('') == nil, 'empty gives nil') -assert(data.iface_addr_parse(nil) == nil, 'nil gives nil') - -- === Calendar ============================================================= -- The fixture uses generic event titles. This repository is public: a real -- appointment names a real person or a real appointment, and neither belongs @@ -479,33 +243,6 @@ assert(up.upgrade == 3 and up.bump == 1 and up.current == 46 and up.missing == 1 'updates summary') assert(data.nvtable_parse('') == nil, 'updates: empty cache is nil') --- drm_vram_parse: xe clients counted once across their fds, other drivers --- skipped, units honoured. -local fdinfo = table.concat({ - '/proc/1/fdinfo/5:drm-driver:\txe', - '/proc/1/fdinfo/5:drm-client-id:\t7', - '/proc/1/fdinfo/5:drm-resident-vram0:\t1024 KiB', - '/proc/1/fdinfo/6:drm-driver:\txe', - '/proc/1/fdinfo/6:drm-client-id:\t7', - '/proc/1/fdinfo/6:drm-resident-vram0:\t1024 KiB', - '/proc/2/fdinfo/9:drm-driver:\txe', - '/proc/2/fdinfo/9:drm-client-id:\t8', - '/proc/2/fdinfo/9:drm-resident-vram0:\t2 MiB', - '/proc/3/fdinfo/4:drm-driver:\tamdgpu', - '/proc/3/fdinfo/4:drm-client-id:\t9', - '/proc/3/fdinfo/4:drm-resident-vram0:\t5 GiB', -}, '\n') -assert(data.drm_vram_parse(fdinfo) == 3 * 1024 ^ 2, - 'drm vram: got ' .. tostring(data.drm_vram_parse(fdinfo))) -assert(data.drm_vram_parse('') == nil, 'drm vram: no clients 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') - -- Emoji fall back to card.FONT_EMOJI: the toy API has no per-glyph fallback, -- so without the run split "x🦷" measured a tofu box in Noto Sans. do -- cgit v1.2.3