358 lines
12 KiB
QML
358 lines
12 KiB
QML
pragma Singleton
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.Networking
|
|
|
|
QtObject {
|
|
id: root
|
|
|
|
property string status: "..."
|
|
|
|
property ListModel activeConnections: ListModel {}
|
|
|
|
property string downloadSpeed: ""
|
|
property string uploadSpeed: ""
|
|
property string ping: ""
|
|
|
|
property var downloadHistory: []
|
|
property var uploadHistory: []
|
|
property var pingHistory: []
|
|
property int _maxHistory: 60
|
|
property bool internetAvailable: Networking.connectivity === NetworkConnectivity.Full
|
|
property bool vpnConnected: false
|
|
property bool vpnExists: false
|
|
property string vpnName: ""
|
|
property string vpnIp: ""
|
|
property bool tailscaleConnected: false
|
|
property bool tailscaleExists: false
|
|
property string tailscaleIp: ""
|
|
|
|
property bool running: false
|
|
|
|
property real _prevRxBytes: 0
|
|
property real _prevTxBytes: 0
|
|
property var _prevUpdateTime: 0
|
|
property string _trafficActiveDevice: ""
|
|
property string _lastNetworkSignature: ""
|
|
|
|
readonly property var networking: Networking
|
|
|
|
function findAllConnectedDevices() {
|
|
var devs = networking.devices.values
|
|
var result = []
|
|
for (var i = 0; i < devs.length; i++) {
|
|
if (devs[i].connected) result.push(devs[i])
|
|
}
|
|
return result
|
|
}
|
|
|
|
function getNmConnectionName(net) {
|
|
if (!net) return ""
|
|
var settings = net.nmSettings
|
|
if (settings.length > 0) return settings[0].id
|
|
return ""
|
|
}
|
|
|
|
function updateAll() {
|
|
updateNetworks(true)
|
|
updateStats()
|
|
}
|
|
|
|
function updateNetworks(force) {
|
|
var connected = findAllConnectedDevices()
|
|
|
|
var sig = ""
|
|
for (var i = 0; i < connected.length; i++) {
|
|
var dev = connected[i]
|
|
sig += dev.name + ":" + dev.type + ":" + dev.connected
|
|
if (dev.type === DeviceType.Wifi) {
|
|
var nets = dev.networks.values
|
|
for (var j = 0; j < nets.length; j++) {
|
|
if (nets[j].connected) {
|
|
sig += ":" + nets[j].name
|
|
break
|
|
}
|
|
}
|
|
}
|
|
sig += ";"
|
|
}
|
|
|
|
if (!force && sig === _lastNetworkSignature) return
|
|
_lastNetworkSignature = sig
|
|
|
|
activeConnections.clear()
|
|
var hasWifi = false
|
|
var hasWired = false
|
|
var bestWifiSignal = 0
|
|
var trafficDev = connected.length > 0 ? connected[0] : null
|
|
|
|
for (var i = 0; i < connected.length; i++) {
|
|
var dev = connected[i]
|
|
var entry = {
|
|
interfaceName: dev.name,
|
|
interfaceType: "",
|
|
connectionName: "",
|
|
wifiName: "",
|
|
signalStrength: 0,
|
|
linkSpeed: "",
|
|
ipv4: ""
|
|
}
|
|
|
|
if (dev.type === DeviceType.Wifi) {
|
|
entry.interfaceType = "wifi"
|
|
hasWifi = true
|
|
var networks = dev.networks.values
|
|
for (var j = 0; j < networks.length; j++) {
|
|
var net = networks[j]
|
|
if (net.connected) {
|
|
entry.wifiName = net.name
|
|
entry.signalStrength = Math.round(net.signalStrength * 100)
|
|
entry.connectionName = root.getNmConnectionName(net)
|
|
if (net.signalStrength > bestWifiSignal) bestWifiSignal = net.signalStrength
|
|
break
|
|
}
|
|
}
|
|
} else if (dev.type === DeviceType.Wired) {
|
|
entry.interfaceType = "ethernet"
|
|
hasWired = true
|
|
entry.linkSpeed = dev.linkSpeed + " Mbps"
|
|
if (dev.network) {
|
|
entry.connectionName = root.getNmConnectionName(dev.network)
|
|
}
|
|
}
|
|
|
|
activeConnections.append(entry)
|
|
|
|
if (dev.type === DeviceType.Wired && trafficDev !== dev) {
|
|
trafficDev = dev
|
|
}
|
|
}
|
|
|
|
if (trafficDev) {
|
|
_trafficActiveDevice = trafficDev.name
|
|
}
|
|
|
|
root.computeStatus(hasWired, hasWifi, bestWifiSignal)
|
|
|
|
if (connected.length > 0) {
|
|
ipv4Script.running = true
|
|
}
|
|
|
|
networkDetailScript.running = true
|
|
}
|
|
|
|
function updateStats() {
|
|
if (!_trafficActiveDevice) return
|
|
statsScript.running = true
|
|
}
|
|
|
|
property Timer networkTimer: Timer {
|
|
interval: 5000
|
|
repeat: true
|
|
running: true
|
|
onTriggered: root.updateNetworks()
|
|
}
|
|
|
|
property Timer statsTimer: Timer {
|
|
interval: 1000
|
|
repeat: true
|
|
running: true
|
|
onTriggered: root.updateStats()
|
|
}
|
|
|
|
property Process ipv4Script: Process {
|
|
command: ["sh", "-c",
|
|
"ip -4 addr show 2>/dev/null | awk '/^[0-9]+:/{iface=$2; gsub(/:.*/, \"\", iface)} /inet /{print iface\" \"$2}' | " +
|
|
"grep -v '\\blo\\b' | grep -v '\\bvirbr' | grep -v '\\btailscale' | grep -v '\\bdocker' | grep -v '\\bveth' | " +
|
|
"sed 's|/[0-9]*||'"
|
|
]
|
|
stdout: StdioCollector {
|
|
onStreamFinished: root.parseIpv4Output(text.trim())
|
|
}
|
|
}
|
|
|
|
function parseIpv4Output(output) {
|
|
var ipMap = {}
|
|
if (!output) {
|
|
root.applyIpv4Map(ipMap)
|
|
return
|
|
}
|
|
var lines = output.split("\n")
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var parts = lines[i].trim().split(" ")
|
|
if (parts.length >= 2) {
|
|
ipMap[parts[0]] = parts[1]
|
|
}
|
|
}
|
|
root.applyIpv4Map(ipMap)
|
|
}
|
|
|
|
function applyIpv4Map(ipMap) {
|
|
for (var i = 0; i < activeConnections.count; i++) {
|
|
var iface = activeConnections.get(i).interfaceName
|
|
var ip = ipMap[iface] || ""
|
|
activeConnections.setProperty(i, "ipv4", ip)
|
|
}
|
|
}
|
|
|
|
property Process networkDetailScript: Process {
|
|
command: ["sh", "-c",
|
|
"echo '===VPN_ALL==='; " +
|
|
"nmcli -t -f NAME,TYPE connection show 2>/dev/null | grep -i vpn || true; " +
|
|
"echo '===VPN_ACTIVE==='; " +
|
|
"nmcli -t -f NAME,TYPE connection show --active 2>/dev/null | grep -i vpn || true; " +
|
|
"echo '===TAILSCALE_EXISTS==='; " +
|
|
"ip link show tailscale0 >/dev/null 2>&1 && echo EXISTS || echo ''; " +
|
|
"echo '===TAILSCALE_IP==='; " +
|
|
"ip -4 addr show tailscale0 2>/dev/null | grep -oP 'inet \\K[\\d.]+' || echo ''"
|
|
]
|
|
|
|
stdout: StdioCollector {
|
|
onStreamFinished: root.parseNetworkDetails(text.trim())
|
|
}
|
|
}
|
|
|
|
function parseNetworkDetails(output) {
|
|
var sections = output.split("===TAILSCALE_IP===")
|
|
var tailscaleIpSection = sections.length > 1 ? sections[1].trim() : ""
|
|
|
|
var beforeTsIp = sections[0]
|
|
sections = beforeTsIp.split("===TAILSCALE_EXISTS===")
|
|
var tailscaleExistsSection = sections.length > 1 ? sections[1].trim() : ""
|
|
|
|
var beforeTsExist = sections[0]
|
|
sections = beforeTsExist.split("===VPN_ACTIVE===")
|
|
var vpnActiveSection = sections.length > 1 ? sections[1].trim() : ""
|
|
|
|
var beforeVpnActive = sections[0]
|
|
sections = beforeVpnActive.split("===VPN_ALL===")
|
|
var vpnAllSection = sections.length > 1 ? sections[1].trim() : ""
|
|
|
|
root.vpnExists = vpnAllSection.length > 0
|
|
if (vpnActiveSection) {
|
|
root.vpnConnected = true
|
|
var vpnLines = vpnActiveSection.split("\n")
|
|
if (vpnLines.length > 0) {
|
|
root.vpnName = vpnLines[0].split(":")[0] || ""
|
|
}
|
|
} else {
|
|
root.vpnConnected = false
|
|
root.vpnName = ""
|
|
root.vpnIp = ""
|
|
}
|
|
|
|
root.tailscaleExists = tailscaleExistsSection.indexOf("EXISTS") >= 0
|
|
if (tailscaleIpSection) {
|
|
root.tailscaleConnected = true
|
|
root.tailscaleIp = tailscaleIpSection
|
|
} else {
|
|
root.tailscaleConnected = false
|
|
root.tailscaleIp = ""
|
|
}
|
|
}
|
|
|
|
property Process statsScript: Process {
|
|
command: ["sh", "-c",
|
|
"echo '===TRAFFIC==='; " +
|
|
"DEV=\"" + root._trafficActiveDevice + "\"; " +
|
|
"if [ -n \"$DEV\" ]; then " +
|
|
" echo \"RX_BYTES=$(cat /sys/class/net/$DEV/statistics/rx_bytes 2>/dev/null || echo 0)\"; " +
|
|
" echo \"TX_BYTES=$(cat /sys/class/net/$DEV/statistics/tx_bytes 2>/dev/null || echo 0)\"; " +
|
|
"fi; " +
|
|
"echo '===PING==='; " +
|
|
"ping -c 1 -W 2 1.1.1.1 2>/dev/null | awk -F/ '/rtt/{print $5}' || echo ''"
|
|
]
|
|
|
|
stdout: StdioCollector {
|
|
onStreamFinished: root.parseStats(text.trim())
|
|
}
|
|
}
|
|
|
|
function parseStats(output) {
|
|
var sections = output.split("===PING===")
|
|
root.ping = sections.length > 1 ? sections[1].trim() : ""
|
|
|
|
var pingVal = parseFloat(root.ping)
|
|
if (!isNaN(pingVal) && pingVal > 0) {
|
|
var pingHist = root.pingHistory.slice()
|
|
pingHist.push(pingVal)
|
|
if (pingHist.length > root._maxHistory) pingHist.shift()
|
|
root.pingHistory = pingHist
|
|
}
|
|
|
|
var trafficSection = sections[0].replace("===TRAFFIC===", "").trim()
|
|
|
|
if (trafficSection) {
|
|
var trafficLines = trafficSection.split("\n")
|
|
var rxBytes = 0
|
|
var txBytes = 0
|
|
for (var t = 0; t < trafficLines.length; t++) {
|
|
var tline = trafficLines[t].trim()
|
|
if (tline.indexOf("RX_BYTES=") === 0) rxBytes = parseFloat(tline.substring("RX_BYTES=".length))
|
|
else if (tline.indexOf("TX_BYTES=") === 0) txBytes = parseFloat(tline.substring("TX_BYTES=".length))
|
|
}
|
|
|
|
if (root._prevRxBytes > 0 && root._prevUpdateTime > 0) {
|
|
var now = Date.now()
|
|
var interval = (now - root._prevUpdateTime) / 1000
|
|
if (interval > 0) {
|
|
var dlBytes = (rxBytes - root._prevRxBytes) / interval
|
|
var ulBytes = (txBytes - root._prevTxBytes) / interval
|
|
root.downloadSpeed = root.formatSpeed(dlBytes)
|
|
root.uploadSpeed = root.formatSpeed(ulBytes)
|
|
|
|
var dlHist = root.downloadHistory.slice()
|
|
dlHist.push(dlBytes)
|
|
if (dlHist.length > root._maxHistory) dlHist.shift()
|
|
root.downloadHistory = dlHist
|
|
|
|
var ulHist = root.uploadHistory.slice()
|
|
ulHist.push(ulBytes)
|
|
if (ulHist.length > root._maxHistory) ulHist.shift()
|
|
root.uploadHistory = ulHist
|
|
}
|
|
} else {
|
|
root.downloadSpeed = ""
|
|
root.uploadSpeed = ""
|
|
}
|
|
|
|
root._prevRxBytes = rxBytes
|
|
root._prevTxBytes = txBytes
|
|
root._prevUpdateTime = Date.now()
|
|
}
|
|
}
|
|
|
|
function computeStatus(hasWired, hasWifi, bestSignal) {
|
|
if (!hasWired && !hasWifi) {
|
|
root.status = "/"
|
|
return
|
|
}
|
|
|
|
var parts = []
|
|
if (hasWired) parts.push("")
|
|
if (hasWifi) {
|
|
var sig = Math.round(bestSignal * 100)
|
|
if (sig > 80) parts.push("")
|
|
else if (sig > 60) parts.push("")
|
|
else if (sig > 40) parts.push("")
|
|
else if (sig > 20) parts.push("")
|
|
else if (sig > 0) parts.push("")
|
|
else parts.push("")
|
|
}
|
|
|
|
root.status = parts.join(" ")
|
|
}
|
|
|
|
function formatSpeed(bytesPerSec) {
|
|
if (bytesPerSec >= 1073741824) return (bytesPerSec / 1073741824).toFixed(1) + " GB/s"
|
|
if (bytesPerSec >= 1048576) return (bytesPerSec / 1048576).toFixed(1) + " MB/s"
|
|
if (bytesPerSec >= 1024) return (bytesPerSec / 1024).toFixed(1) + " KB/s"
|
|
return bytesPerSec.toFixed(0) + " B/s"
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
root.updateAll()
|
|
}
|
|
}
|