From d734110cc2627cd08ab220bcecfd147056c4250b Mon Sep 17 00:00:00 2001 From: Hannes Date: Fri, 21 Aug 2026 23:30:28 +0200 Subject: [PATCH] updates to networking --- modules/bar/states/Network/NetworkState.qml | 346 ++++++++++++-------- modules/bar/widgets/Network.qml | 246 +++++++------- modules/bar/widgets/Sparkline.qml | 67 ++++ 3 files changed, 392 insertions(+), 267 deletions(-) create mode 100644 modules/bar/widgets/Sparkline.qml diff --git a/modules/bar/states/Network/NetworkState.qml b/modules/bar/states/Network/NetworkState.qml index 96d500e..da9993f 100644 --- a/modules/bar/states/Network/NetworkState.qml +++ b/modules/bar/states/Network/NetworkState.qml @@ -2,77 +2,211 @@ pragma Singleton import QtQuick import Quickshell import Quickshell.Io -import "../../../../config" +import Quickshell.Networking QtObject { id: root property string status: "..." - property string interfaceName: "" - property string interfaceType: "" - property string connectionName: "" - property string ipv4: "" - property string wifiName: "" - property string signalStrength: "" - property string wifiChannel: "" - property string wifiFrequency: "" - property string linkSpeed: "" + property ListModel activeConnections: ListModel {} + property string downloadSpeed: "" property string uploadSpeed: "" property string ping: "" - property bool internetAvailable: false + + 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 string _activeDevice: "" property real _prevRxBytes: 0 property real _prevTxBytes: 0 property var _prevUpdateTime: 0 + property string _trafficActiveDevice: "" + + 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 "" + } + + property var _pendingDevices: [] + + function updateAll() { + var connected = findAllConnectedDevices() + _pendingDevices = connected + + 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 + trafficScript.running = true + } + + root.computeStatus(hasWired, hasWifi, bestWifiSignal) + + if (connected.length > 0) { + ipv4Script.running = true + } + } property Timer networkTimer: Timer { interval: 10000 repeat: true running: true - onTriggered: networkScript.running = true + onTriggered: root.updateAll() } - property Process networkScript: Process { + 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 trafficScript: Process { command: ["sh", "-c", - "echo '===SHOW==='; " + - "ACTIVE=$(nmcli -t device status 2>/dev/null | awk -F: '$3==\"connected\"{print $1; exit}'); " + - "echo \"ACTIVE_DEVICE=$ACTIVE\"; " + - "if [ -n \"$ACTIVE\" ]; then " + - " nmcli -t -f ALL device show \"$ACTIVE\" 2>/dev/null; " + - "fi; " + "echo '===TRAFFIC==='; " + - "if [ -n \"$ACTIVE\" ]; then " + - " echo \"RX_BYTES=$(cat /sys/class/net/$ACTIVE/statistics/rx_bytes 2>/dev/null || echo 0)\"; " + - " echo \"TX_BYTES=$(cat /sys/class/net/$ACTIVE/statistics/tx_bytes 2>/dev/null || echo 0)\"; " + + "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 '===WIFI==='; " + - "nmcli -t -f active,ssid,signal,chan,freq,rate dev wifi | awk -F: '$1==\"yes\" { $1=\"\"; sub(/^:/,\"\"); print }'; " + "echo '===PING==='; " + "ping -c 1 -W 2 1.1.1.1 2>/dev/null | awk -F/ '/rtt/{print $5}' || echo ''; " + - "echo '===INTERNET==='; " + - "ping -c 1 -W 1 1.1.1.1 2>/dev/null >/dev/null && echo 1 || echo 0; " + - "echo '===VPN==='; " + + "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==='; " + - "TAIL_IP=$(ip -4 addr show tailscale0 2>/dev/null | grep -oP 'inet \\K[\\d.]+'); " + - "if [ -n \"$TAIL_IP\" ]; then echo \"TAILSCALE_IP=$TAIL_IP\"; else echo \"\"; fi" + "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.parseNetworkOutput(text.trim()) + onStreamFinished: root.parseOutput(text.trim()) } } + 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" @@ -80,54 +214,38 @@ QtObject { return bytesPerSec.toFixed(0) + " B/s" } - function parseNetworkOutput(output) { - var sections = output.split("===TAILSCALE===") - var tailscaleSection = sections.length > 1 ? sections[1].trim() : "" + function parseOutput(output) { + var sections = output.split("===TAILSCALE_IP===") + var tailscaleIpSection = sections.length > 1 ? sections[1].trim() : "" - var beforeTailscale = sections[0] - sections = beforeTailscale.split("===VPN===") - var vpnSection = sections.length > 1 ? sections[1].trim() : "" + var beforeTsIp = sections[0] + sections = beforeTsIp.split("===TAILSCALE_EXISTS===") + var tailscaleExistsSection = sections.length > 1 ? sections[1].trim() : "" - var beforeVpn = sections[0] - sections = beforeVpn.split("===INTERNET===") - root.internetAvailable = sections.length > 1 ? sections[1].trim() === "1" : false + var beforeTsExist = sections[0] + sections = beforeTsExist.split("===VPN_ACTIVE===") + var vpnActiveSection = sections.length > 1 ? sections[1].trim() : "" - var beforeInternet = sections[0] - sections = beforeInternet.split("===PING===") + var beforeVpnActive = sections[0] + sections = beforeVpnActive.split("===VPN_ALL===") + var vpnAllSection = sections.length > 1 ? sections[1].trim() : "" + + var beforeVpnAll = sections[0] + sections = beforeVpnAll.split("===PING===") root.ping = sections.length > 1 ? sections[1].trim() : "" - var beforePing = sections[0] - sections = beforePing.split("===WIFI===") - var wifiSection = sections.length > 1 ? sections[1].trim() : "" - - var beforeWifi = sections[0] - sections = beforeWifi.split("===TRAFFIC===") - var trafficSection = sections.length > 1 ? sections[1].trim() : "" - - var beforeTraffic = sections[0] - sections = beforeTraffic.split("===SHOW===") - var showSection = sections.length > 1 ? sections[1].trim() : "" - - // Parse SHOW section - var activeDevice = "" - var lines = showSection.split("\n") - for (var i = 0; i < lines.length; i++) { - var line = lines[i].trim() - if (line.indexOf("ACTIVE_DEVICE=") === 0) { - activeDevice = line.substring("ACTIVE_DEVICE=".length) - } else if (line.indexOf("GENERAL.DEVICE:") === 0) { - root.interfaceName = line.substring("GENERAL.DEVICE:".length) - } else if (line.indexOf("GENERAL.TYPE:") === 0) { - root.interfaceType = line.substring("GENERAL.TYPE:".length) - } else if (line.indexOf("GENERAL.CONNECTION:") === 0) { - root.connectionName = line.substring("GENERAL.CONNECTION:".length) - } else if (line.indexOf("IP4.ADDRESS") >= 0) { - var ipPart = line.substring(line.indexOf(":") + 1) - root.ipv4 = ipPart.split("/")[0] - } + 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 } - // Parse TRAFFIC section + var beforePing = sections[0] + sections = beforePing.split("===TRAFFIC===") + var trafficSection = sections.length > 1 ? sections[1].trim() : "" + if (trafficSection) { var trafficLines = trafficSection.split("\n") var rxBytes = 0 @@ -138,75 +256,41 @@ QtObject { else if (tline.indexOf("TX_BYTES=") === 0) txBytes = parseFloat(tline.substring("TX_BYTES=".length)) } - // console.log("[Network] rxBytes:", rxBytes, "txBytes:", txBytes) - // console.log("[Network] activeDevice:", activeDevice, "| prevActiveDevice:", root._activeDevice) - // console.log("[Network] prevRxBytes:", root._prevRxBytes, "prevTxBytes:", root._prevTxBytes, "prevTime:", root._prevUpdateTime) - - if (activeDevice === root._activeDevice && root._prevRxBytes > 0 && root._prevUpdateTime > 0) { + if (root._prevRxBytes > 0 && root._prevUpdateTime > 0) { var now = Date.now() var interval = (now - root._prevUpdateTime) / 1000 - // console.log("[Network] Same device, interval:", interval, "s") - // console.log("[Network] rxDelta:", (rxBytes - root._prevRxBytes), "txDelta:", (txBytes - root._prevTxBytes)) if (interval > 0) { - var downBps = (rxBytes - root._prevRxBytes) / interval - var upBps = (txBytes - root._prevTxBytes) / interval - // console.log("[Network] downBps:", downBps, "upBps:", upBps) - root.downloadSpeed = root.formatSpeed(downBps) - root.uploadSpeed = root.formatSpeed(upBps) - // console.log("[Network] downloadSpeed:", root.downloadSpeed, "uploadSpeed:", root.uploadSpeed) - } else { - console.log("[Network] Interval zero or negative, skipping") + 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 { - // console.log("[Network] First run or device changed, storing baseline") root.downloadSpeed = "" root.uploadSpeed = "" } - root._activeDevice = activeDevice root._prevRxBytes = rxBytes root._prevTxBytes = txBytes root._prevUpdateTime = Date.now() - // console.log("[Network] Stored baseline: rxBytes:", root._prevRxBytes, "txBytes:", root._prevTxBytes) } - // Parse WIFI section - if (wifiSection) { - var wifiParts = wifiSection.split(" ") - if (wifiParts.length >= 2) { - root.wifiName = wifiParts[0] || "" - root.signalStrength = wifiParts[1] || "" - if (wifiParts.length >= 3) root.wifiChannel = wifiParts[2] || "" - if (wifiParts.length >= 4) root.wifiFrequency = wifiParts[3] + " " + wifiParts[4] || "" - if (wifiParts.length >= 5) root.linkSpeed = wifiParts[5] + " " + wifiParts[6] || "" - } - } - - // Determine status icon - if (root.interfaceType === "ethernet") { - root.status = "󰈀" - } else if (root.interfaceType === "wifi") { - if (!root.connectionName) { - root.status = "󰤭" - } else { - var sig = parseInt(root.signalStrength) - if (sig > 80) root.status = "󰤨" - else if (sig > 60) root.status = "󰤥" - else if (sig > 40) root.status = "󰤢" - else if (sig > 20) root.status = "󰤟" - else root.status = "󰤯" - } - } else if (!activeDevice) { - root.status = "󰤭/󱐤" - } - - // Parse VPN section - if (vpnSection) { + root.vpnExists = vpnAllSection.length > 0 + if (vpnActiveSection) { root.vpnConnected = true - var vpnLines = vpnSection.split("\n") + var vpnLines = vpnActiveSection.split("\n") if (vpnLines.length > 0) { - var vpnParts = vpnLines[0].split(":") - root.vpnName = vpnParts[0] || "" + root.vpnName = vpnLines[0].split(":")[0] || "" } } else { root.vpnConnected = false @@ -214,10 +298,10 @@ QtObject { root.vpnIp = "" } - // Parse TAILSCALE section - if (tailscaleSection && tailscaleSection.indexOf("TAILSCALE_IP=") === 0) { + root.tailscaleExists = tailscaleExistsSection.indexOf("EXISTS") >= 0 + if (tailscaleIpSection) { root.tailscaleConnected = true - root.tailscaleIp = tailscaleSection.substring("TAILSCALE_IP=".length) + root.tailscaleIp = tailscaleIpSection } else { root.tailscaleConnected = false root.tailscaleIp = "" @@ -225,6 +309,6 @@ QtObject { } Component.onCompleted: { - networkScript.running = true + root.updateAll() } -} \ No newline at end of file +} diff --git a/modules/bar/widgets/Network.qml b/modules/bar/widgets/Network.qml index 273d1d2..c8ae6f7 100644 --- a/modules/bar/widgets/Network.qml +++ b/modules/bar/widgets/Network.qml @@ -1,5 +1,4 @@ import QtQuick -import Quickshell import "../../../config" import "../states/Network" @@ -12,9 +11,8 @@ MouseArea { cursorShape: Qt.PointingHandCursor onClicked: { - console.log("[Network] Refreshing... current status: " + NetworkState.status) - NetworkState.networkScript.running = false - NetworkState.networkScript.running = true + console.log("[Network] Manual refresh") + NetworkState.updateAll() } Rectangle { @@ -44,167 +42,143 @@ MouseArea { Column { spacing: parent.spacing - Text { - text: "interfaceName: " + NetworkState.interfaceName - wrapMode: Text.WordWrap - color: Colors.primary - anchors.horizontalCenter: parent.horizontalCenter - font.pixelSize: Config.font_size_tiny(bar.height) - font.family: Config.font + + Repeater { + model: NetworkState.activeConnections + delegate: Column { + spacing: 2 + Text { + text: model.interfaceType === "wifi" ? "Wifi" : "Ethernet" + color: Colors.secondary + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + font.bold: true + } + Text { + text: " interface: " + model.interfaceName + color: Colors.primary + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + } + Text { + text: " connection: " + model.connectionName + color: Colors.primary + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + } + Text { + text: " ipv4: " + model.ipv4 + color: Colors.primary + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + visible: model.ipv4 !== "" + } + Text { + text: " wifi: " + model.wifiName + " signal: " + model.signalStrength + "%" + color: Colors.primary + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + visible: model.interfaceType === "wifi" + } + Text { + text: " linkSpeed: " + model.linkSpeed + color: Colors.primary + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + visible: model.interfaceType === "ethernet" + } + } + } + + Item { width: 1; height: parent.spacing } + + Column { + width: parent.width + spacing: 2 + Text { + text: "download: " + NetworkState.downloadSpeed + color: Colors.primary + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + } + Sparkline { + values: NetworkState.downloadHistory + lineColor: Colors.primary + width: parent.width + } + } + + Column { + width: parent.width + spacing: 2 + Text { + text: "upload: " + NetworkState.uploadSpeed + color: Colors.primary + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + } + Sparkline { + values: NetworkState.uploadHistory + lineColor: Colors.tertiary + width: parent.width + } + } + + Column { + width: parent.width + spacing: 2 + Text { + text: "ping: " + NetworkState.ping + " ms" + color: Colors.primary + font.pixelSize: Config.font_size_tiny(bar.height) + font.family: Config.font + } + Sparkline { + values: NetworkState.pingHistory + lineColor: Colors.error + width: parent.width + } } Text { - text: "interfaceType: " + NetworkState.interfaceType - wrapMode: Text.WordWrap - color: Colors.primary - anchors.horizontalCenter: parent.horizontalCenter + text: "no connection" + color: Colors.error font.pixelSize: Config.font_size_tiny(bar.height) font.family: Config.font + visible: !NetworkState.internetAvailable } Text { - text: "connectionName: " + NetworkState.connectionName - wrapMode: Text.WordWrap + text: "VPN: disconnected" color: Colors.primary - anchors.horizontalCenter: parent.horizontalCenter font.pixelSize: Config.font_size_tiny(bar.height) font.family: Config.font + visible: NetworkState.vpnExists && !NetworkState.vpnConnected } Text { - text: "ipv4: " + NetworkState.ipv4 - wrapMode: Text.WordWrap + text: "VPN: " + NetworkState.vpnName color: Colors.primary - anchors.horizontalCenter: parent.horizontalCenter font.pixelSize: Config.font_size_tiny(bar.height) font.family: Config.font + visible: NetworkState.vpnConnected } Text { - text: "wifiName: " + NetworkState.wifiName - wrapMode: Text.WordWrap + text: "Tailscale: disconnected" color: Colors.primary - anchors.horizontalCenter: parent.horizontalCenter font.pixelSize: Config.font_size_tiny(bar.height) font.family: Config.font + visible: NetworkState.tailscaleExists && !NetworkState.tailscaleConnected } Text { - text: "signalStrength: " + NetworkState.signalStrength - wrapMode: Text.WordWrap + text: "Tailscale: " + NetworkState.tailscaleIp color: Colors.primary - anchors.horizontalCenter: parent.horizontalCenter - font.pixelSize: Config.font_size_tiny(bar.height) - font.family: Config.font - } - - Text { - text: "wifiChannel: " + NetworkState.wifiChannel - wrapMode: Text.WordWrap - color: Colors.primary - anchors.horizontalCenter: parent.horizontalCenter - font.pixelSize: Config.font_size_tiny(bar.height) - font.family: Config.font - } - - Text { - text: "wifiFrequency: " + NetworkState.wifiFrequency - wrapMode: Text.WordWrap - color: Colors.primary - anchors.horizontalCenter: parent.horizontalCenter - font.pixelSize: Config.font_size_tiny(bar.height) - font.family: Config.font - } - - Text { - text: "linkSpeed: " + NetworkState.linkSpeed - wrapMode: Text.WordWrap - color: Colors.primary - anchors.horizontalCenter: parent.horizontalCenter - font.pixelSize: Config.font_size_tiny(bar.height) - font.family: Config.font - } - - Text { - text: "downloadSpeed: " + NetworkState.downloadSpeed - wrapMode: Text.WordWrap - color: Colors.primary - anchors.horizontalCenter: parent.horizontalCenter - font.pixelSize: Config.font_size_tiny(bar.height) - font.family: Config.font - } - - Text { - text: "uploadSpeed: " + NetworkState.uploadSpeed - wrapMode: Text.WordWrap - color: Colors.primary - anchors.horizontalCenter: parent.horizontalCenter - font.pixelSize: Config.font_size_tiny(bar.height) - font.family: Config.font - } - - Text { - text: "ping: " + NetworkState.ping - wrapMode: Text.WordWrap - color: Colors.primary - anchors.horizontalCenter: parent.horizontalCenter - font.pixelSize: Config.font_size_tiny(bar.height) - font.family: Config.font - } - - Text { - text: "internetAvailable: " + NetworkState.internetAvailable - wrapMode: Text.WordWrap - color: Colors.primary - anchors.horizontalCenter: parent.horizontalCenter - font.pixelSize: Config.font_size_tiny(bar.height) - font.family: Config.font - } - - Text { - text: "vpnConnected: " + NetworkState.vpnConnected - wrapMode: Text.WordWrap - color: Colors.primary - anchors.horizontalCenter: parent.horizontalCenter - font.pixelSize: Config.font_size_tiny(bar.height) - font.family: Config.font - } - - Text { - text: "vpnName: " + NetworkState.vpnName - wrapMode: Text.WordWrap - color: Colors.primary - anchors.horizontalCenter: parent.horizontalCenter - font.pixelSize: Config.font_size_tiny(bar.height) - font.family: Config.font - } - - Text { - text: "vpnIp: " + NetworkState.vpnIp - wrapMode: Text.WordWrap - color: Colors.primary - anchors.horizontalCenter: parent.horizontalCenter - font.pixelSize: Config.font_size_tiny(bar.height) - font.family: Config.font - } - - Text { - text: "tailscaleConnected: " + NetworkState.tailscaleConnected - wrapMode: Text.WordWrap - color: Colors.primary - anchors.horizontalCenter: parent.horizontalCenter - font.pixelSize: Config.font_size_tiny(bar.height) - font.family: Config.font - } - - Text { - text: "tailscaleIp: " + NetworkState.tailscaleIp - wrapMode: Text.WordWrap - color: Colors.primary - anchors.horizontalCenter: parent.horizontalCenter font.pixelSize: Config.font_size_tiny(bar.height) font.family: Config.font + visible: NetworkState.tailscaleConnected } } } -} \ No newline at end of file +} diff --git a/modules/bar/widgets/Sparkline.qml b/modules/bar/widgets/Sparkline.qml new file mode 100644 index 0000000..8048968 --- /dev/null +++ b/modules/bar/widgets/Sparkline.qml @@ -0,0 +1,67 @@ +import QtQuick +import "../../../config" + +Item { + id: root + + property var values: [] + property color lineColor: Colors.primary + property color fillColor: Qt.alpha(Colors.primary, 0.15) + property int pointCount: 60 + property real minWidth: 120 + property real graphHeight: 30 + + width: Math.max(minWidth, parent ? parent.width : minWidth) + height: graphHeight + + Canvas { + id: canvas + anchors.fill: parent + + onPaint: { + var ctx = getContext("2d") + ctx.clearRect(0, 0, width, height) + + var data = root.values + if (!data || data.length < 2) return + + var displayCount = Math.min(data.length, root.pointCount) + var startIdx = data.length - displayCount + var slice = data.slice(startIdx) + + var maxVal = 0 + for (var i = 0; i < slice.length; i++) { + if (slice[i] > maxVal) maxVal = slice[i] + } + if (maxVal === 0) maxVal = 1 + + var stepX = width / (slice.length - 1) + var padY = 2 + var drawH = height - padY * 2 + + ctx.beginPath() + ctx.moveTo(0, height - padY - (slice[0] / maxVal) * drawH) + for (var j = 1; j < slice.length; j++) { + var x = j * stepX + var y = height - padY - (slice[j] / maxVal) * drawH + ctx.lineTo(x, y) + } + ctx.strokeStyle = root.lineColor + ctx.lineWidth = 1.5 + ctx.stroke() + + ctx.lineTo(width, height) + ctx.lineTo(0, height) + ctx.closePath() + ctx.fillStyle = root.fillColor + ctx.fill() + } + + Connections { + target: root + function onValuesChanged() { canvas.requestPaint() } + function onWidthChanged() { canvas.requestPaint() } + function onHeightChanged() { canvas.requestPaint() } + } + } +}