* Make omarchy-network-qr detect the connected Wi-Fi interface The interface argument is now optional so IPC and menu callers can summon the QR card without knowing the device name. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Move the speed test into a modal card with cluster dials The network panel's Run button and the new omarchy.network speedTest IPC route open a centered card where download and upload dials sweep on open and track the live readings, Tucson style. Dismissing the card stops the traffic workers. The QR card gains a showQr IPC route. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add Setup > Network menu with DNS, QR Code, and Speed Test DNS switches providers through omarchy-dns with the current choice checked. QR Code only shows while connected over Wi-Fi. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Hold the speed test card steady and add a corner dismiss The Run Again button now fades instead of unmounting so the card keeps its size across runs, and a small X in the corner closes the card alongside Esc and the scrim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Move the speed test action into the network panel hero A speedometer icon beside the QR share replaces the dedicated inline section, and the keyboard chain loses its speed stop accordingly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Float the speed test cluster on the scrim Drop the bordered card and the pulsing halo: like the Tucson's floating cluster, the dials now sit directly on a near-black scrim with a soft under-glow along the value arc, fainter ticks, and hubless needles that fade toward the pivot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Center the retry button between the dials The measuring status lines and the corner dismiss go away; the retry button moves into the gap between the two dials like a cluster's center display, anchored out of the column flow so nothing ever shifts. The fast.com attribution lives on as its tooltip, and only errors still print below the cluster. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Put the retry button back beneath the dial pair The dials close ranks again and the retry button returns below them, centered on the pair and still fading in place so nothing shifts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Harden the network IPC routes against stale panel state The QR menu route forces interface self-detection instead of trusting details that stop refreshing while the panel is closed, and the widget's canonical close now tears down the centered cards and their traffic instead of only hiding the compact panel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Re-range the speed dials for every run The scale latched upward forever, so one unusually fast run would compress every later one for the lifetime of the shell process. Each dial now returns to the base scale when its measurement starts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Float the Wi-Fi QR share like the speed test Same presentation as the dials: no bordered card, just the code on a heavy scrim. Only the dark modules paint now, so the white canvas can round its corners while the spec quiet zone keeps the code clear. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Pick the default-route device and pin the locale in QR detection nmcli localizes state names, so the detection fallback pins LC_ALL=C and accepts states like "connected (externally)". Detection now prefers the default-route device, matching the connection the panel and the menu's visibility gate describe when several Wi-Fi adapters are up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Make every network summon path overlay-aware Opening the widget while a centered card is up now dismisses the card instead of raising the compact panel behind an exclusive overlay -- the shadowed open() covers the keybind toggle, the bar icon, and IPC. Re-summoning a card while its process is still tearing down queues the fresh request for onExited instead of dropping it, and the speed test route refreshes connection details like the QR route does. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Shrink the overlays to fit narrow outputs The dial cluster and the QR card scale down instead of clipping when a portrait or heavily scaled display is narrower than their natural size. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
148 lines
8.5 KiB
Bash
148 lines
8.5 KiB
Bash
#!/bin/bash
|
||
|
||
set -euo pipefail
|
||
|
||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||
|
||
run_node_test <<'JS'
|
||
const fs = require('fs')
|
||
const network = requireFromRoot('shell/plugins/panels/network/Model.js')
|
||
const panelSource = fs.readFileSync(root + '/shell/plugins/panels/network/Panel.qml', 'utf8')
|
||
|
||
assert(/IpcHandler[\s\S]*?function toggleNetwork\(\) \{ root\.toggleNetwork\(\) \}/.test(panelSource), 'network exposes the Wi-Fi radio toggle over IPC')
|
||
assert(/manageIpc: false/.test(panelSource), 'network owns its IPC handler so it can extend the target methods')
|
||
|
||
assertDeepEqual(
|
||
network.parseNetworkStatus('wifi\tCafe WiFi\t78\t5200\n'),
|
||
{ kind: 'wifi', label: 'Cafe WiFi', signalStrength: 78, frequency: '5200' },
|
||
'network parses bar status'
|
||
)
|
||
assertEqual(network.connectionIcon('wifi', 80), network.wifiIconFor(80), 'network maps wifi icon from signal')
|
||
assertEqual(network.formatHeaderSpeed('1000'), '1gbit', 'network formats gigabit speed')
|
||
assertEqual(network.formatHeaderSpeed('2500'), '2.5gbit', 'network formats fractional gigabit speed')
|
||
assertEqual(network.formatHeaderFreq('2462'), '2.4ghz', 'network formats 2.4GHz wifi band')
|
||
assertEqual(network.formatHeaderFreq('5200'), '5ghz', 'network formats 5GHz wifi band')
|
||
assertEqual(network.formatHeaderFreq('6455.0'), '6ghz', 'network formats 6GHz wifi band')
|
||
assertEqual(network.formatHeaderFreq('18300'), '18.3ghz', 'network falls back to exact GHz for unknown bands')
|
||
assertEqual(network.headerDetail({ type: 'ethernet', speed: '100' }), '100mbit', 'network header uses ethernet speed')
|
||
|
||
assertDeepEqual(
|
||
network.parseKeyValue('iface\twlan0\nrx_bytes\t100\ntx_bytes\t50\n'),
|
||
{ iface: 'wlan0', rx_bytes: '100', tx_bytes: '50' },
|
||
'network parses detail key values'
|
||
)
|
||
assertEqual(network.decodeIwSsid('Cafe\\xe2\\x80\\x99'), 'Cafe’', 'network decodes UTF-8 SSID bytes')
|
||
assertEqual(network.decodeIwSsid('Smile \\xf0\\x9f\\x98\\x80'), 'Smile 😀', 'network decodes emoji SSID bytes')
|
||
assertEqual(network.decodeIwSsid('\\x20Cafe\\x20'), ' Cafe ', 'network preserves edge spaces in SSIDs')
|
||
assertEqual(network.decodeIwSsid('slash\\x5cname'), 'slash\\name', 'network decodes SSID backslashes once')
|
||
assertEqual(network.decodeIwSsid('invalid\\xff'), 'invalid\\xff', 'network preserves invalid UTF-8 escapes')
|
||
assertEqual(network.decodeIwSsid('already 😀'), 'already 😀', 'network safely preserves unexpected non-BMP input')
|
||
assertDeepEqual(
|
||
network.parseKeyValue('ssid\tline\\x0abreak\\x09tab\\x00nul\nsignal_dbm\t-40\n'),
|
||
{ ssid: 'line\\x0abreak\\x09tab\\x00nul', signal_dbm: '-40' },
|
||
'network leaves control-byte escapes safe for single-line display'
|
||
)
|
||
assertDeepEqual(
|
||
network.throughputState({ prevIface: '', prevSampleTime: 0 }, { iface: 'wlan0', rx_bytes: '100', tx_bytes: '50' }, 10),
|
||
{ prevIface: 'wlan0', prevRxBytes: 100, prevTxBytes: 50, prevSampleTime: 10, downloadRate: 0, uploadRate: 0 },
|
||
'network seeds throughput state on first sample'
|
||
)
|
||
assertDeepEqual(
|
||
network.throughputState({ prevIface: 'wlan0', prevRxBytes: 100, prevTxBytes: 50, prevSampleTime: 10 }, { iface: 'wlan0', rx_bytes: '300', tx_bytes: '90' }, 12),
|
||
{ prevIface: 'wlan0', prevRxBytes: 300, prevTxBytes: 90, prevSampleTime: 12, downloadRate: 100, uploadRate: 20 },
|
||
'network computes throughput deltas'
|
||
)
|
||
|
||
let ping = network.pingLatencyState(
|
||
{ pingIface: '', routerPingSamples: [], internetPingSamples: [] },
|
||
{ iface: 'wlan0', router_ping_ms: '2.0', internet_ping_ms: '20.0' },
|
||
4
|
||
)
|
||
assertDeepEqual(
|
||
ping,
|
||
{ pingIface: 'wlan0', routerPingSamples: [2], internetPingSamples: [20], routerPingLatency: 2, internetPingLatency: 20, internetPingPacketLoss: 0 },
|
||
'network seeds ping latency samples'
|
||
)
|
||
|
||
ping = network.pingLatencyState(ping, { iface: 'wlan0', router_ping_ms: '4.0', internet_ping_ms: '' }, 4)
|
||
assertDeepEqual(
|
||
ping,
|
||
{ pingIface: 'wlan0', routerPingSamples: [2, 4], internetPingSamples: [20, null], routerPingLatency: 3, internetPingLatency: 20, internetPingPacketLoss: 50 },
|
||
'network averages recent successful ping samples'
|
||
)
|
||
|
||
assertDeepEqual(
|
||
network.pingLatencyState(ping, { iface: 'eth0', router_ping_ms: '1.5', internet_ping_ms: '10.0' }, 4),
|
||
{ pingIface: 'eth0', routerPingSamples: [1.5], internetPingSamples: [10], routerPingLatency: 1.5, internetPingLatency: 10, internetPingPacketLoss: 0 },
|
||
'network resets ping samples when interface changes'
|
||
)
|
||
|
||
assertDeepEqual(
|
||
network.pingLatencyState(ping, { iface: 'wlan0', internet_ping_ms: '22.0' }, 4),
|
||
{ pingIface: 'wlan0', routerPingSamples: [], internetPingSamples: [20, null, 22], routerPingLatency: -1, internetPingLatency: 21, internetPingPacketLoss: 33 },
|
||
'network clears ping samples when a target is unavailable'
|
||
)
|
||
|
||
assertEqual(network.formatBytes(1536), '1.5 KB', 'network formats bytes')
|
||
assertEqual(network.formatRate(1536), '1.5 KB/s', 'network formats rates')
|
||
assertEqual(network.formatPingLatency('2.54'), '2.5 ms', 'network formats low ping with precision')
|
||
assertEqual(network.formatPingLatency('25.4'), '25 ms', 'network formats ping')
|
||
assertEqual(network.formatPingLatency(''), 'Timeout', 'network formats missing ping as timeout')
|
||
assertEqual(network.formatPingLatency(-1, false), '--', 'network holds the ping row before the first sample')
|
||
assertEqual(network.formatPingLatency('25.4', true), '25 ms', 'network formats ping once samples exist')
|
||
assertEqual(network.formatPingLatency('', true), 'Timeout', 'network still reports a timeout among real samples')
|
||
assertEqual(network.formatPacketLoss(2), '2%', 'network formats packet loss')
|
||
assertEqual(network.formatPacketLoss(0), '0%', 'network formats zero packet loss')
|
||
assertEqual(network.formatPacketLoss(0, false), '--', 'network holds the packet loss row before the first sample')
|
||
assertEqual(network.formatPacketLoss(0, true), '0%', 'network reports zero loss once samples exist')
|
||
|
||
const rows = network.sortWifiRows([
|
||
{ ssid: 'Open', connected: false, known: false, signal: 95 },
|
||
{ ssid: 'Known', connected: false, known: true, signal: 10 },
|
||
{ ssid: 'Connected', connected: true, known: true, signal: 20 }
|
||
])
|
||
assertDeepEqual(rows.map(row => row.ssid), ['Connected', 'Known', 'Open'], 'network sorts wifi rows by connection and known state')
|
||
assertEqual(network.wifiSectionTitle(rows, 0), 'KNOWN NETWORKS', 'network labels known wifi section')
|
||
assertEqual(network.wifiSectionTitle(rows, 2), 'OTHER NETWORKS', 'network labels other wifi section')
|
||
|
||
assertDeepEqual(
|
||
network.parseQrMatrix('010\n111\n010\n'),
|
||
{ rows: ['010', '111', '010'], size: 3 },
|
||
'network parses a square QR matrix'
|
||
)
|
||
assertDeepEqual(network.parseQrMatrix('01\n111\n'), { rows: [], size: 0 }, 'network rejects ragged QR rows')
|
||
assertDeepEqual(network.parseQrMatrix('010\n101\n'), { rows: [], size: 0 }, 'network rejects a non-square QR matrix')
|
||
assertDeepEqual(network.parseQrMatrix('010\n1x1\n010\n'), { rows: [], size: 0 }, 'network rejects invalid QR modules')
|
||
|
||
const reasons = { NoSecrets: 1, WifiAuthTimeout: 2, WifiNetworkLost: 3, WifiClientDisconnected: 4, WifiClientFailed: 5 }
|
||
assertEqual(network.networkFailureReason(1, reasons), 'Passphrase required', 'network maps missing passphrase failures')
|
||
assertEqual(network.networkFailureReason(2, reasons), 'Wrong password', 'network maps auth timeout failures')
|
||
assertEqual(network.networkFailureReason(99, reasons), 'Failed to connect', 'network maps unknown failures')
|
||
|
||
|
||
assertEqual(network.bandLabel('2.4'), '2.4ghz', 'network labels the 2.4GHz band')
|
||
assertEqual(network.bandLabel('6'), '6ghz', 'network labels the 6GHz band')
|
||
assertEqual(network.bandLabel('auto'), 'Auto', 'network labels the automatic band choice')
|
||
|
||
assertEqual(network.bandSectionTitle('auto', '2.4'), 'WI-FI BAND: 2.4GHZ', 'network names the live band in the header under automatic')
|
||
assertEqual(network.bandSectionTitle('auto', ''), 'WI-FI BAND', 'network omits an unknown band from the header')
|
||
assertEqual(network.bandSectionTitle('5', '5'), 'WI-FI BAND', 'network drops the header band once the pills are showing')
|
||
assertEqual(network.bandSectionTitle('5', '2.4'), 'WI-FI BAND', 'network keeps a plain header while a pin is settling')
|
||
|
||
assertDeepEqual(
|
||
network.parseBandStatus('band\t5\navailable\t2.4 5 6\nselected\tauto\n'),
|
||
{ band: '5', selected: 'auto', available: ['2.4', '5', '6'] },
|
||
'network parses band status'
|
||
)
|
||
assertDeepEqual(
|
||
network.parseBandStatus(''),
|
||
{ band: '', selected: 'auto', available: [] },
|
||
'network parses empty band status without a wifi connection'
|
||
)
|
||
|
||
|
||
|
||
assertEqual(network.headerDetail({ type: 'wifi', freq: '5745' }), '', 'network keeps wifi band state out of the hero')
|
||
assertEqual(network.headerDetail({ type: 'ethernet', speed: '100' }), '100mbit', 'network keeps ethernet speed in the hero')
|
||
JS
|