203 lines
5.5 KiB
QML
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()
|
|
}
|
|
}
|