Add ping to network panel
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
# omarchy:args=[--verbose]
|
||||
|
||||
verbose=false
|
||||
internet_probe=1.1.1.1
|
||||
|
||||
case "${1:-}" in
|
||||
"")
|
||||
@@ -21,7 +22,7 @@ esac
|
||||
print_status() {
|
||||
local device nm state ssid signal freq
|
||||
|
||||
device=$(ip route get 1.1.1.1 2>/dev/null | awk '{ for (i = 1; i <= NF; i++) if ($i == "dev") { print $(i + 1); exit } }')
|
||||
device=$(ip route get "$internet_probe" 2>/dev/null | awk '{ for (i = 1; i <= NF; i++) if ($i == "dev") { print $(i + 1); exit } }')
|
||||
|
||||
if [[ -z $device ]]; then
|
||||
printf 'disconnected\t\t\t\n'
|
||||
@@ -52,10 +53,44 @@ print_status() {
|
||||
printf 'wifi\t%s\t\t\n' "$device"
|
||||
}
|
||||
|
||||
ping_latency_ms() {
|
||||
local host=$1
|
||||
|
||||
LC_ALL=C ping -n -c 1 -W 1 "$host" 2>/dev/null | awk -F'time[=<]' '/time[=<]/ { split($2, parts, " "); print parts[1]; exit }'
|
||||
}
|
||||
|
||||
print_ping_samples() {
|
||||
local gateway=$1
|
||||
local tmpdir router_file internet_file
|
||||
local router_pid="" internet_pid=""
|
||||
|
||||
omarchy-cmd-present ping || return
|
||||
tmpdir=$(mktemp -d) || return
|
||||
router_file="$tmpdir/router"
|
||||
internet_file="$tmpdir/internet"
|
||||
|
||||
if [[ -n $gateway ]]; then
|
||||
ping_latency_ms "$gateway" >"$router_file" &
|
||||
router_pid=$!
|
||||
fi
|
||||
|
||||
ping_latency_ms "$internet_probe" >"$internet_file" &
|
||||
internet_pid=$!
|
||||
|
||||
if [[ -n $router_pid ]]; then
|
||||
wait "$router_pid"
|
||||
printf 'router_ping_ms\t%s\n' "$(cat "$router_file")"
|
||||
fi
|
||||
|
||||
wait "$internet_pid"
|
||||
printf 'internet_ping_ms\t%s\n' "$(cat "$internet_file")"
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
print_verbose() {
|
||||
local route_json iface gw src prefix link
|
||||
|
||||
route_json=$(ip -j route get 1.1.1.1 2>/dev/null)
|
||||
route_json=$(ip -j route get "$internet_probe" 2>/dev/null)
|
||||
[[ -z $route_json ]] && return
|
||||
|
||||
iface=$(jq -r '.[0].dev // ""' <<<"$route_json" 2>/dev/null)
|
||||
@@ -96,6 +131,8 @@ print_verbose() {
|
||||
[[ -r /sys/class/net/$iface/speed ]] && printf 'speed\t%s\n' "$(cat /sys/class/net/$iface/speed)"
|
||||
[[ -r /sys/class/net/$iface/duplex ]] && printf 'duplex\t%s\n' "$(cat /sys/class/net/$iface/duplex)"
|
||||
fi
|
||||
|
||||
print_ping_samples "$gw"
|
||||
}
|
||||
|
||||
if [[ $verbose == "true" ]]; then
|
||||
|
||||
@@ -97,6 +97,57 @@ function throughputState(previous, next, now) {
|
||||
}
|
||||
}
|
||||
|
||||
function pingSampleValue(raw) {
|
||||
var value = parseFloat(raw)
|
||||
if (!isFinite(value) || value < 0) return null
|
||||
return value
|
||||
}
|
||||
|
||||
function appendPingSample(samples, raw, limit) {
|
||||
var values = Array.isArray(samples) ? samples.slice() : []
|
||||
|
||||
values.push(pingSampleValue(raw))
|
||||
while (values.length > limit) values.shift()
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
function averagePingLatency(samples) {
|
||||
var values = Array.isArray(samples) ? samples : []
|
||||
var total = 0
|
||||
var count = 0
|
||||
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var value = values[i]
|
||||
if (typeof value !== "number" || !isFinite(value) || value < 0) continue
|
||||
total += value
|
||||
count++
|
||||
}
|
||||
|
||||
return count > 0 ? total / count : -1
|
||||
}
|
||||
|
||||
function pingLatencyState(previous, next, limit) {
|
||||
var prev = previous || {}
|
||||
var sample = next || {}
|
||||
var iface = sample.iface || ""
|
||||
var window = Math.max(1, parseInt(limit, 10) || 5)
|
||||
var reset = iface === "" || iface !== (prev.pingIface || "")
|
||||
var routerSamples = reset ? [] : prev.routerPingSamples
|
||||
var internetSamples = reset ? [] : prev.internetPingSamples
|
||||
|
||||
routerSamples = sample.router_ping_ms === undefined ? [] : appendPingSample(routerSamples, sample.router_ping_ms, window)
|
||||
internetSamples = sample.internet_ping_ms === undefined ? [] : appendPingSample(internetSamples, sample.internet_ping_ms, window)
|
||||
|
||||
return {
|
||||
pingIface: iface,
|
||||
routerPingSamples: routerSamples,
|
||||
internetPingSamples: internetSamples,
|
||||
routerPingLatency: averagePingLatency(routerSamples),
|
||||
internetPingLatency: averagePingLatency(internetSamples)
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
var n = Number(bytes)
|
||||
if (!isFinite(n) || n < 0) n = 0
|
||||
@@ -110,6 +161,12 @@ function formatRate(bytesPerSec) {
|
||||
return formatBytes(bytesPerSec) + "/s"
|
||||
}
|
||||
|
||||
function formatPingLatency(ms) {
|
||||
var value = parseFloat(ms)
|
||||
if (!isFinite(value) || value < 0) return "Timeout"
|
||||
return value.toFixed(value > 0 && value < 10 ? 1 : 0) + " ms"
|
||||
}
|
||||
|
||||
function wifiRow(network) {
|
||||
if (!network) return null
|
||||
return {
|
||||
@@ -168,8 +225,10 @@ if (typeof module !== "undefined") {
|
||||
headerDetail: headerDetail,
|
||||
parseKeyValue: parseKeyValue,
|
||||
throughputState: throughputState,
|
||||
pingLatencyState: pingLatencyState,
|
||||
formatBytes: formatBytes,
|
||||
formatRate: formatRate,
|
||||
formatPingLatency: formatPingLatency,
|
||||
wifiRow: wifiRow,
|
||||
sortWifiRows: sortWifiRows,
|
||||
wifiSectionTitle: wifiSectionTitle,
|
||||
|
||||
@@ -20,7 +20,7 @@ Panel {
|
||||
}
|
||||
|
||||
// Live connection details from `ip` / /sys / iw.
|
||||
property var info: ({}) // { iface, type, ip, prefix, gateway, speed, duplex, ssid, signal, freq, bitrate, rx_bytes, tx_bytes }
|
||||
property var info: ({}) // { iface, type, ip, prefix, gateway, speed, duplex, ssid, signal, freq, bitrate, rx_bytes, tx_bytes, router_ping_ms, internet_ping_ms }
|
||||
|
||||
// Throughput tracking. Rates are computed as deltas between successive
|
||||
// `omarchy-network-status --verbose` samples (~1.5s apart via detailsPoll).
|
||||
@@ -32,6 +32,18 @@ Panel {
|
||||
property string prevIface: ""
|
||||
property real downloadRate: 0 // bytes/sec
|
||||
property real uploadRate: 0 // bytes/sec
|
||||
property string pingIface: ""
|
||||
property var routerPingSamples: []
|
||||
property var internetPingSamples: []
|
||||
property real routerPingLatency: -1
|
||||
property real internetPingLatency: -1
|
||||
readonly property int pingWindow: 5
|
||||
readonly property bool hasRouterPing: routerPingSamples.length > 0
|
||||
readonly property bool hasInternetPing: internetPingSamples.length > 0
|
||||
readonly property bool hasPing: hasRouterPing || hasInternetPing
|
||||
readonly property bool hasSecondPing: hasRouterPing && hasInternetPing
|
||||
readonly property string primaryPingLabel: hasRouterPing ? "Router Ping" : (hasInternetPing ? "Internet Ping" : "")
|
||||
readonly property real primaryPingLatency: hasRouterPing ? routerPingLatency : internetPingLatency
|
||||
property int connectionPhraseIndex: 0
|
||||
readonly property var connectionPhrases: [
|
||||
"Wiring bits",
|
||||
@@ -141,6 +153,11 @@ Panel {
|
||||
prevSampleTime = 0
|
||||
downloadRate = 0
|
||||
uploadRate = 0
|
||||
pingIface = ""
|
||||
routerPingSamples = []
|
||||
internetPingSamples = []
|
||||
routerPingLatency = -1
|
||||
internetPingLatency = -1
|
||||
if (wifiDevice) wifiDevice.scannerEnabled = false
|
||||
}
|
||||
}
|
||||
@@ -279,6 +296,7 @@ Panel {
|
||||
var next = Model.parseKeyValue(raw)
|
||||
info = next
|
||||
updateThroughput(next)
|
||||
updatePingLatency(next)
|
||||
}
|
||||
|
||||
function updateThroughput(next) {
|
||||
@@ -299,6 +317,20 @@ Panel {
|
||||
uploadRate = state.uploadRate
|
||||
}
|
||||
|
||||
function updatePingLatency(next) {
|
||||
var state = Model.pingLatencyState({
|
||||
pingIface: pingIface,
|
||||
routerPingSamples: routerPingSamples,
|
||||
internetPingSamples: internetPingSamples
|
||||
}, next, pingWindow)
|
||||
|
||||
pingIface = state.pingIface
|
||||
routerPingSamples = state.routerPingSamples
|
||||
internetPingSamples = state.internetPingSamples
|
||||
routerPingLatency = state.routerPingLatency
|
||||
internetPingLatency = state.internetPingLatency
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
return Model.formatBytes(bytes)
|
||||
}
|
||||
@@ -307,6 +339,10 @@ Panel {
|
||||
return Model.formatRate(bytesPerSec)
|
||||
}
|
||||
|
||||
function formatPingLatency(ms) {
|
||||
return Model.formatPingLatency(ms)
|
||||
}
|
||||
|
||||
function findDevice(type) {
|
||||
var devices = networkDevices || []
|
||||
for (var i = 0; i < devices.length; i++) {
|
||||
@@ -802,6 +838,23 @@ Panel {
|
||||
columnSpacing: Style.space(20)
|
||||
rowSpacing: Style.spacing.labelGap
|
||||
|
||||
InfoLabel {
|
||||
visible: root.hasPing
|
||||
text: root.primaryPingLabel
|
||||
}
|
||||
DetailValue {
|
||||
visible: root.hasPing
|
||||
text: root.formatPingLatency(root.primaryPingLatency)
|
||||
}
|
||||
InfoLabel {
|
||||
visible: root.hasPing
|
||||
text: root.hasSecondPing ? "Internet Ping" : ""
|
||||
}
|
||||
DetailValue {
|
||||
visible: root.hasPing
|
||||
text: root.hasSecondPing ? root.formatPingLatency(root.internetPingLatency) : ""
|
||||
}
|
||||
|
||||
InfoLabel { visible: root.info.rx_bytes !== undefined; text: "Receiving" }
|
||||
DetailValue { visible: root.info.rx_bytes !== undefined; text: root.formatRate(root.downloadRate) }
|
||||
InfoLabel { visible: root.info.rx_bytes !== undefined; text: "Sending" }
|
||||
|
||||
@@ -37,8 +37,41 @@ assertDeepEqual(
|
||||
'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 },
|
||||
'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 },
|
||||
'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 },
|
||||
'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 },
|
||||
'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')
|
||||
|
||||
const rows = network.sortWifiRows([
|
||||
{ ssid: 'Open', connected: false, known: false, signal: 95 },
|
||||
|
||||
Reference in New Issue
Block a user