Files
blomarchy/shell/plugins/panels/speedtest/Panel.qml
T

203 lines
5.5 KiB
QML

import QtQuick
import Quickshell.Io
import qs.Commons
import qs.Ui
// The shared gauge-cluster overlay (SpeedTestOverlay) dressed for the
// internet speed test: download and upload dials in Mbps, titled with the
// connection under test.
//
// Standalone panel plugin: summoning it starts a fresh run, dismissing it
// stops the traffic, so the download workers never keep saturating the link
// behind a closed overlay. The payload may carry the connection's display
// name -- {"connection": "MyWifi"} -- and the panel looks it up itself via
// blob-network-status when the caller doesn't know it.
Item {
id: root
property var shell: null
property var manifest: null
property bool opened: false
property string connectionName: ""
property bool running: false
property bool expectedStop: false
property bool pendingRun: false
property string phase: "" // "down" | "up" | ""
property string stderrText: ""
property string downloadMbps: ""
property string uploadMbps: ""
property string error: ""
readonly property real downloadValue: toMbps(downloadMbps)
readonly property real uploadValue: toMbps(uploadMbps)
function toMbps(raw) {
var value = parseFloat(raw)
return isFinite(value) && value > 0 ? value : 0
}
function open(payloadJson) {
var payload = {}
try { payload = JSON.parse(payloadJson || "{}") || {} } catch (e) {}
if (payload.connection !== undefined) root.connectionName = String(payload.connection)
else refreshConnectionName()
root.opened = true
runSpeedTest()
}
function close() {
root.opened = false
root.pendingRun = false
phaseTimer.stop()
// Clear the phase before killing the process: onExited advances to the
// upload phase when it still reads "down".
root.phase = ""
root.running = false
if (speedTestProc.running) {
root.expectedStop = true
speedTestProc.running = false
}
}
function dismiss() {
if (root.shell && typeof root.shell.hide === "function")
root.shell.hide((root.manifest && root.manifest.id) || "blob.speedtest")
else close()
}
function refreshConnectionName() {
root.connectionName = ""
statusProc.running = false
statusProc.running = true
}
function updateSpeedTestLine(line) {
var value = parseFloat(line)
if (!isFinite(value) || value < 0) return
if (phase === "down") downloadMbps = String(value)
else if (phase === "up") uploadMbps = String(value)
}
function runSpeedTest() {
if (speedTestProc.running) {
// A dismissal's SIGTERM is still in flight; Process.running stays true
// until the child exits, so queue the fresh run for onExited.
if (expectedStop) pendingRun = true
return
}
error = ""
downloadMbps = ""
uploadMbps = ""
running = true
startPhase("down")
}
function startPhase(nextPhase) {
expectedStop = false
phase = nextPhase
stderrText = ""
speedTestProc.command = ["blob-network-speedtest", nextPhase]
speedTestProc.running = true
phaseTimer.restart()
}
function stopPhase() {
phaseTimer.stop()
if (speedTestProc.running) {
expectedStop = true
speedTestProc.running = false
return
}
finishPhase()
}
function finishPhase() {
if (phase === "down") {
startPhase("up")
return
}
phase = ""
running = false
expectedStop = false
}
Process {
id: speedTestProc
stdout: SplitParser { onRead: function(line) { root.updateSpeedTestLine(line) } }
// Exit and stream-finished have no guaranteed order: when a failed exit
// beat the collector and published the generic message, replace it with
// the specific one once it lands.
stderr: StdioCollector {
waitForEnd: true
onStreamFinished: {
root.stderrText = String(text || "").trim()
if (root.error !== "" && root.stderrText !== "") root.error = root.stderrText
}
}
onExited: function(exitCode) {
phaseTimer.stop()
if (root.pendingRun) {
root.pendingRun = false
root.expectedStop = false
if (root.opened) Qt.callLater(root.runSpeedTest)
return
}
if (!root.expectedStop && exitCode !== 0) {
root.error = root.stderrText || "Speed test failed"
root.phase = ""
root.running = false
return
}
root.expectedStop = false
root.finishPhase()
}
}
Timer {
id: phaseTimer
interval: 5000
repeat: false
onTriggered: root.stopPhase()
}
// Names the connection under test when the summoner didn't. First tab
// field is the kind, second the SSID (wifi) or device (ethernet).
Process {
id: statusProc
command: ["blob-network-status"]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: {
var fields = String(text || "").trim().split("\t")
if (fields[0] === "wifi") root.connectionName = fields[1] || "Wi-Fi"
else if (fields[0] === "ethernet") root.connectionName = "Ethernet"
}
}
}
SpeedTestOverlay {
fontFamily: Style.font.family
layerNamespace: "blob-network-speedtest"
title: root.connectionName
leftLabel: "DOWNLOAD"
rightLabel: "UPLOAD"
runAgainTooltip: "Measure again via fast.com"
running: root.running
leftValue: root.downloadValue
rightValue: root.uploadValue
leftLive: root.running && root.phase === "down"
rightLive: root.running && root.phase === "up"
error: root.error
open: root.opened
onCloseRequested: root.dismiss()
onRunAgainRequested: root.runSpeedTest()
}
}