Extract the speed test into its own omarchy.speedtest panel plugin
The network panel now summons it, so a clone or a third-party plugin declaring clonedFrom: omarchy.speedtest can replace the whole speed test -- dials and run orchestration alike -- for every caller. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
4519451655
commit
dd2f434a7d
@@ -118,7 +118,7 @@
|
|||||||
"setup.network.dns.google": {"icon":"","label":"Google","checked":"[[ \"$(omarchy-dns)\" == \"Google\" ]]","action":"omarchy-dns Google"},
|
"setup.network.dns.google": {"icon":"","label":"Google","checked":"[[ \"$(omarchy-dns)\" == \"Google\" ]]","action":"omarchy-dns Google"},
|
||||||
"setup.network.dns.custom": {"icon":"","label":"Custom","checked":"[[ \"$(omarchy-dns)\" == \"Custom\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-dns Custom'"},
|
"setup.network.dns.custom": {"icon":"","label":"Custom","checked":"[[ \"$(omarchy-dns)\" == \"Custom\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-dns Custom'"},
|
||||||
"setup.network.qr": {"icon":"","label":"QR Code","aliases":["wifi-qr"],"when":"[[ $(omarchy-network-status) == wifi* ]]","action":"omarchy-shell omarchy.network showQr"},
|
"setup.network.qr": {"icon":"","label":"QR Code","aliases":["wifi-qr"],"when":"[[ $(omarchy-network-status) == wifi* ]]","action":"omarchy-shell omarchy.network showQr"},
|
||||||
"setup.network.speedtest": {"icon":"","label":"Speed Test","aliases":["speedtest","speed-test"],"action":"omarchy-shell omarchy.network speedTest"},
|
"setup.network.speedtest": {"icon":"","label":"Speed Test","aliases":["speedtest","speed-test"],"action":"omarchy-shell shell summon omarchy.speedtest"},
|
||||||
"setup.default": {"icon":"","label":"Defaults","aliases":["default","defaults"]},
|
"setup.default": {"icon":"","label":"Defaults","aliases":["default","defaults"]},
|
||||||
"setup.default.agent": {"icon":"","label":"Agent"},
|
"setup.default.agent": {"icon":"","label":"Agent"},
|
||||||
"setup.default.agent.claude": {"icon":"","label":"Claude","checked":"[[ \"$(omarchy-default-agent)\" == \"claude\" ]]","action":"omarchy-default-agent claude"},
|
"setup.default.agent.claude": {"icon":"","label":"Claude","checked":"[[ \"$(omarchy-default-agent)\" == \"claude\" ]]","action":"omarchy-default-agent claude"},
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Panel {
|
|||||||
manageIpc: false
|
manageIpc: false
|
||||||
|
|
||||||
// Centralized close so callers can't forget to drop the passphrase prompt.
|
// Centralized close so callers can't forget to drop the passphrase prompt.
|
||||||
readonly property bool overlayVisible: qrVisible || speedTestModalOpen
|
readonly property bool overlayVisible: qrVisible
|
||||||
|
|
||||||
// Shadows the base open(): a summon or toggle while a centered card is up
|
// Shadows the base open(): a summon or toggle while a centered card is up
|
||||||
// dismisses the card instead of opening the compact panel behind an
|
// dismisses the card instead of opening the compact panel behind an
|
||||||
@@ -26,7 +26,6 @@ Panel {
|
|||||||
function open() {
|
function open() {
|
||||||
if (overlayVisible) {
|
if (overlayVisible) {
|
||||||
hideWifiQr()
|
hideWifiQr()
|
||||||
hideSpeedTest()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
root.controller.show()
|
root.controller.show()
|
||||||
@@ -35,10 +34,9 @@ Panel {
|
|||||||
function close() {
|
function close() {
|
||||||
root.controller.hide()
|
root.controller.hide()
|
||||||
cancelPasswordPrompt()
|
cancelPasswordPrompt()
|
||||||
// The centered cards outlive the compact panel, but the widget's
|
// The centered card outlives the compact panel, but the widget's
|
||||||
// canonical close must not leave an overlay (or its traffic) behind.
|
// canonical close must not leave an overlay behind.
|
||||||
hideWifiQr()
|
hideWifiQr()
|
||||||
hideSpeedTest()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelPasswordPrompt() {
|
function cancelPasswordPrompt() {
|
||||||
@@ -101,15 +99,6 @@ Panel {
|
|||||||
property string bandSelected: "auto"
|
property string bandSelected: "auto"
|
||||||
property var bandAvailable: []
|
property var bandAvailable: []
|
||||||
property string pendingBand: ""
|
property string pendingBand: ""
|
||||||
property bool speedTestRunning: false
|
|
||||||
property bool speedTestModalOpen: false
|
|
||||||
property bool speedTestExpectedStop: false
|
|
||||||
property bool pendingSpeedRun: false
|
|
||||||
property string speedTestPhase: ""
|
|
||||||
property string speedTestStderr: ""
|
|
||||||
property string speedTestDownloadMbps: ""
|
|
||||||
property string speedTestUploadMbps: ""
|
|
||||||
property string speedTestError: ""
|
|
||||||
|
|
||||||
// Per-row in-flight state. `actionSsid` flips on for the row whose action
|
// Per-row in-flight state. `actionSsid` flips on for the row whose action
|
||||||
// is currently running so it can render "Connecting…" / "Disconnecting…" /
|
// is currently running so it can render "Connecting…" / "Disconnecting…" /
|
||||||
@@ -251,15 +240,12 @@ Panel {
|
|||||||
root.refresh()
|
root.refresh()
|
||||||
root.showWifiQr(true)
|
root.showWifiQr(true)
|
||||||
}
|
}
|
||||||
function speedTest() {
|
function speedTest() { root.summonSpeedTest() }
|
||||||
root.refresh()
|
|
||||||
root.showSpeedTest()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function activateHeader() {
|
function activateHeader() {
|
||||||
if (headerIndex === qrHeaderIndex) showWifiQr()
|
if (headerIndex === qrHeaderIndex) showWifiQr()
|
||||||
else if (headerIndex === speedHeaderIndex) showSpeedTest()
|
else if (headerIndex === speedHeaderIndex) summonSpeedTest()
|
||||||
else if (headerIndex === toggleHeaderIndex) toggleNetwork()
|
else if (headerIndex === toggleHeaderIndex) toggleNetwork()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -691,83 +677,17 @@ Panel {
|
|||||||
actionProc.running = true
|
actionProc.running = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSpeedTestLine(line) {
|
// The speed test is its own panel plugin (omarchy.speedtest) so a
|
||||||
var value = parseFloat(line)
|
// replacement design can take it over; summon() routes to whichever
|
||||||
if (!isFinite(value) || value < 0) return
|
// implementation is enabled. The payload names the connection when this
|
||||||
|
// panel knows it; the plugin looks it up itself otherwise.
|
||||||
if (speedTestPhase === "down") speedTestDownloadMbps = String(value)
|
function summonSpeedTest() {
|
||||||
else if (speedTestPhase === "up") speedTestUploadMbps = String(value)
|
controller.hide()
|
||||||
speedTestError = ""
|
cancelPasswordPrompt()
|
||||||
}
|
var connection = ""
|
||||||
|
if (info.type === "wifi") connection = info.ssid || "Wi-Fi"
|
||||||
// The speed test lives in a centered modal card like the QR share.
|
else if (info.type === "ethernet") connection = "Ethernet"
|
||||||
// Opening it starts a fresh run; dismissing it stops the traffic, so the
|
bar.shell.summon("omarchy.speedtest", connection ? JSON.stringify({ connection: connection }) : "{}")
|
||||||
// download workers never keep saturating the link behind a closed card.
|
|
||||||
function showSpeedTest() {
|
|
||||||
if (!speedTestModalOpen) {
|
|
||||||
speedTestModalOpen = true
|
|
||||||
controller.hide()
|
|
||||||
cancelPasswordPrompt()
|
|
||||||
}
|
|
||||||
runSpeedTest()
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideSpeedTest() {
|
|
||||||
speedTestModalOpen = false
|
|
||||||
pendingSpeedRun = false
|
|
||||||
speedTestPhaseTimer.stop()
|
|
||||||
// Clear the phase before killing the process: onExited advances to the
|
|
||||||
// upload phase when it still reads "down".
|
|
||||||
speedTestPhase = ""
|
|
||||||
speedTestRunning = false
|
|
||||||
if (speedTestProc.running) {
|
|
||||||
speedTestExpectedStop = true
|
|
||||||
speedTestProc.running = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (speedTestExpectedStop) pendingSpeedRun = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
speedTestError = ""
|
|
||||||
speedTestDownloadMbps = ""
|
|
||||||
speedTestUploadMbps = ""
|
|
||||||
speedTestRunning = true
|
|
||||||
startSpeedTestPhase("down")
|
|
||||||
}
|
|
||||||
|
|
||||||
function startSpeedTestPhase(phase) {
|
|
||||||
speedTestExpectedStop = false
|
|
||||||
speedTestPhase = phase
|
|
||||||
speedTestStderr = ""
|
|
||||||
speedTestProc.command = ["omarchy-network-speedtest", phase]
|
|
||||||
speedTestProc.running = true
|
|
||||||
speedTestPhaseTimer.restart()
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopSpeedTestPhase() {
|
|
||||||
speedTestPhaseTimer.stop()
|
|
||||||
if (speedTestProc.running) {
|
|
||||||
speedTestExpectedStop = true
|
|
||||||
speedTestProc.running = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
finishSpeedTestPhase()
|
|
||||||
}
|
|
||||||
|
|
||||||
function finishSpeedTestPhase() {
|
|
||||||
if (speedTestPhase === "down") {
|
|
||||||
startSpeedTestPhase("up")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
speedTestPhase = ""
|
|
||||||
speedTestRunning = false
|
|
||||||
speedTestExpectedStop = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function dnsCommand(provider) {
|
function dnsCommand(provider) {
|
||||||
@@ -1017,42 +937,6 @@ Panel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
|
||||||
id: speedTestProc
|
|
||||||
stdout: SplitParser { onRead: function(line) { root.updateSpeedTestLine(line) } }
|
|
||||||
stderr: StdioCollector {
|
|
||||||
waitForEnd: true
|
|
||||||
onStreamFinished: root.speedTestStderr = String(text || "").trim()
|
|
||||||
}
|
|
||||||
onExited: function(exitCode) {
|
|
||||||
speedTestPhaseTimer.stop()
|
|
||||||
|
|
||||||
if (root.pendingSpeedRun) {
|
|
||||||
root.pendingSpeedRun = false
|
|
||||||
root.speedTestExpectedStop = false
|
|
||||||
if (root.speedTestModalOpen) Qt.callLater(root.runSpeedTest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!root.speedTestExpectedStop && exitCode !== 0) {
|
|
||||||
root.speedTestError = root.speedTestStderr || "Speed test failed"
|
|
||||||
root.speedTestPhase = ""
|
|
||||||
root.speedTestRunning = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
root.speedTestExpectedStop = false
|
|
||||||
root.finishSpeedTestPhase()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: speedTestPhaseTimer
|
|
||||||
interval: 5000
|
|
||||||
repeat: false
|
|
||||||
onTriggered: root.stopSpeedTestPhase()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Action runner for DNS provider changes. Wi-Fi actions use the
|
// Action runner for DNS provider changes. Wi-Fi actions use the
|
||||||
// Quickshell.Networking NetworkManager backend directly.
|
// Quickshell.Networking NetworkManager backend directly.
|
||||||
Process {
|
Process {
|
||||||
@@ -1316,7 +1200,7 @@ Panel {
|
|||||||
hasCursor: root.speedHeaderHasCursor
|
hasCursor: root.speedHeaderHasCursor
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
onHovered: function(on) { if (on) root.setHeaderCursor(root.speedHeaderIndex) }
|
onHovered: function(on) { if (on) root.setHeaderCursor(root.speedHeaderIndex) }
|
||||||
onClicked: root.showSpeedTest()
|
onClicked: root.summonSpeedTest()
|
||||||
}
|
}
|
||||||
|
|
||||||
ToggleSwitch {
|
ToggleSwitch {
|
||||||
@@ -1720,24 +1604,6 @@ Panel {
|
|||||||
onPasswordToggleRequested: root.toggleQrPassword()
|
onPasswordToggleRequested: root.toggleQrPassword()
|
||||||
}
|
}
|
||||||
|
|
||||||
SpeedTestPanel {
|
|
||||||
anchorItem: button
|
|
||||||
bar: root.bar
|
|
||||||
running: root.speedTestRunning
|
|
||||||
phase: root.speedTestPhase
|
|
||||||
downloadMbps: root.speedTestDownloadMbps
|
|
||||||
uploadMbps: root.speedTestUploadMbps
|
|
||||||
error: root.speedTestError
|
|
||||||
connectionName: {
|
|
||||||
if (root.info.type === "wifi") return root.info.ssid || "Wi-Fi"
|
|
||||||
if (root.info.type === "ethernet") return "Ethernet"
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
open: root.speedTestModalOpen
|
|
||||||
onCloseRequested: root.hideSpeedTest()
|
|
||||||
onRunAgainRequested: root.runSpeedTest()
|
|
||||||
}
|
|
||||||
|
|
||||||
// One Wi-Fi band pill. `active` (fill) is the band actually in use and
|
// One Wi-Fi band pill. `active` (fill) is the band actually in use and
|
||||||
// `selected` (bold) is the pinned choice; with Automatic on nothing is
|
// `selected` (bold) is the pinned choice; with Automatic on nothing is
|
||||||
// pinned, so only the live band lights up and the two can no longer read as
|
// pinned, so only the live band lights up and the two can no longer read as
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"name": "Network",
|
"name": "Network",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"author": "Omarchy",
|
"author": "Omarchy",
|
||||||
"description": "Wi-Fi list, connection state, QR sharing, and speed test",
|
"description": "Wi-Fi list, connection state, and QR sharing",
|
||||||
"kinds": [
|
"kinds": [
|
||||||
"bar-widget"
|
"bar-widget"
|
||||||
],
|
],
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
},
|
},
|
||||||
"barWidget": {
|
"barWidget": {
|
||||||
"displayName": "Network",
|
"displayName": "Network",
|
||||||
"description": "Wi-Fi list, connection state, QR sharing, and speed test",
|
"description": "Wi-Fi list, connection state, and QR sharing",
|
||||||
"category": "Network",
|
"category": "Network",
|
||||||
"allowMultiple": false
|
"allowMultiple": false
|
||||||
}
|
}
|
||||||
|
|||||||
+263
-115
@@ -2,6 +2,7 @@ import QtQuick
|
|||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Shapes
|
import QtQuick.Shapes
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Ui
|
import qs.Ui
|
||||||
@@ -12,160 +13,307 @@ import qs.Ui
|
|||||||
// digital readout in the middle. Esc, the scrim, or the corner dismiss close
|
// digital readout in the middle. Esc, the scrim, or the corner dismiss close
|
||||||
// it; the needles sweep to full scale and back on open, then track the live
|
// it; the needles sweep to full scale and back on open, then track the live
|
||||||
// readings.
|
// readings.
|
||||||
PanelWindow {
|
//
|
||||||
|
// 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
|
||||||
|
// omarchy-network-status when the caller doesn't know it.
|
||||||
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property Item anchorItem
|
property string omarchyPath: Quickshell.env("OMARCHY_PATH")
|
||||||
required property QtObject bar
|
property var shell: null
|
||||||
required property bool running
|
property var manifest: null
|
||||||
required property string phase // "down" | "up" | ""
|
|
||||||
required property string downloadMbps
|
|
||||||
required property string uploadMbps
|
|
||||||
required property string error
|
|
||||||
required property string connectionName
|
|
||||||
property bool open: false
|
|
||||||
|
|
||||||
signal closeRequested()
|
property bool opened: false
|
||||||
signal runAgainRequested()
|
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 downloadValue: toMbps(downloadMbps)
|
||||||
readonly property real uploadValue: toMbps(uploadMbps)
|
readonly property real uploadValue: toMbps(uploadMbps)
|
||||||
readonly property bool failed: error !== ""
|
readonly property bool failed: error !== ""
|
||||||
readonly property bool finished: !running && !failed && (downloadValue > 0 || uploadValue > 0)
|
|
||||||
|
|
||||||
// The scrim below is a fixed near-black regardless of theme, so text and
|
// The scrim below is a fixed near-black regardless of theme, so text and
|
||||||
// ticks on it need a fixed light palette, not the themed bar.foreground.
|
// ticks on it need a fixed light palette, not the themed foreground.
|
||||||
readonly property color onScrim: "white"
|
readonly property color onScrim: "white"
|
||||||
readonly property color onScrimDim: Qt.rgba(1, 1, 1, 0.55)
|
readonly property color onScrimDim: Qt.rgba(1, 1, 1, 0.55)
|
||||||
readonly property color onScrimUrgent: "#ff6b6b"
|
readonly property color onScrimUrgent: "#ff6b6b"
|
||||||
|
readonly property string fontFamily: Style.font.family
|
||||||
|
|
||||||
function toMbps(raw) {
|
function toMbps(raw) {
|
||||||
var value = parseFloat(raw)
|
var value = parseFloat(raw)
|
||||||
return isFinite(value) && value > 0 ? value : 0
|
return isFinite(value) && value > 0 ? value : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
visible: open
|
function open(payloadJson) {
|
||||||
// The window is instantiated hidden, so re-acquire focus after mapping and
|
var payload = {}
|
||||||
// fire the ignition sweep once the surface is actually on screen.
|
try { payload = JSON.parse(payloadJson || "{}") || {} } catch (e) {}
|
||||||
onOpenChanged: {
|
if (payload.connection !== undefined) root.connectionName = String(payload.connection)
|
||||||
if (open) Qt.callLater(function() {
|
else refreshConnectionName()
|
||||||
if (!root.open) return
|
root.opened = true
|
||||||
|
runSpeedTest()
|
||||||
|
// The window is instantiated hidden, so re-acquire focus after mapping
|
||||||
|
// and fire the ignition sweep once the surface is actually on screen.
|
||||||
|
Qt.callLater(function() {
|
||||||
|
if (!root.opened) return
|
||||||
keyCatcher.forceActiveFocus()
|
keyCatcher.forceActiveFocus()
|
||||||
downDial.ignite()
|
downDial.ignite()
|
||||||
upDial.ignite()
|
upDial.ignite()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
screen: anchorItem.QsWindow.window ? anchorItem.QsWindow.window.screen : null
|
|
||||||
anchors { top: true; bottom: true; left: true; right: true }
|
|
||||||
color: "transparent"
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
|
||||||
WlrLayershell.namespace: "omarchy-network-speedtest"
|
|
||||||
WlrLayershell.layer: WlrLayer.Overlay
|
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
|
||||||
|
|
||||||
// Deep scrim: with no card behind them, the floating dials need the
|
function close() {
|
||||||
// backdrop to carry the contrast on any wallpaper, like the near-black
|
root.opened = false
|
||||||
// panel behind a real cluster.
|
root.pendingRun = false
|
||||||
Rectangle {
|
phaseTimer.stop()
|
||||||
anchors.fill: parent
|
// Clear the phase before killing the process: onExited advances to the
|
||||||
color: Qt.rgba(0, 0, 0, 0.78)
|
// upload phase when it still reads "down".
|
||||||
|
root.phase = ""
|
||||||
MouseArea {
|
root.running = false
|
||||||
anchors.fill: parent
|
if (speedTestProc.running) {
|
||||||
onClicked: root.closeRequested()
|
root.expectedStop = true
|
||||||
|
speedTestProc.running = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
function dismiss() {
|
||||||
id: keyCatcher
|
if (root.shell && typeof root.shell.hide === "function")
|
||||||
anchors.fill: parent
|
root.shell.hide((root.manifest && root.manifest.id) || "omarchy.speedtest")
|
||||||
focus: true
|
else close()
|
||||||
|
}
|
||||||
|
|
||||||
Keys.onEscapePressed: root.closeRequested()
|
function refreshConnectionName() {
|
||||||
Keys.onReturnPressed: if (!root.running) root.runAgainRequested()
|
root.connectionName = ""
|
||||||
Keys.onEnterPressed: if (!root.running) root.runAgainRequested()
|
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)
|
||||||
|
error = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = ["omarchy-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) } }
|
||||||
|
stderr: StdioCollector {
|
||||||
|
waitForEnd: true
|
||||||
|
onStreamFinished: root.stderrText = String(text || "").trim()
|
||||||
|
}
|
||||||
|
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: ["omarchy-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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
visible: root.opened
|
||||||
|
anchors { top: true; bottom: true; left: true; right: true }
|
||||||
|
color: "transparent"
|
||||||
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
WlrLayershell.namespace: "omarchy-network-speedtest"
|
||||||
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||||
|
|
||||||
|
// Deep scrim: with no card behind them, the floating dials need the
|
||||||
|
// backdrop to carry the contrast on any wallpaper, like the near-black
|
||||||
|
// panel behind a real cluster.
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.78)
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: root.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: cluster
|
id: keyCatcher
|
||||||
anchors.centerIn: parent
|
anchors.fill: parent
|
||||||
width: content.implicitWidth
|
focus: true
|
||||||
height: content.implicitHeight
|
|
||||||
// Narrow or heavily scaled outputs: shrink the whole cluster rather
|
|
||||||
// than clipping it at the screen edge.
|
|
||||||
scale: Math.min(1,
|
|
||||||
(keyCatcher.width - Style.space(32)) / Math.max(1, width),
|
|
||||||
(keyCatcher.height - Style.space(32)) / Math.max(1, height))
|
|
||||||
|
|
||||||
// Swallow clicks so only the scrim outside the cluster dismisses.
|
Keys.onEscapePressed: root.dismiss()
|
||||||
MouseArea { anchors.fill: parent; onClicked: {} }
|
Keys.onReturnPressed: if (!root.running) root.runSpeedTest()
|
||||||
|
Keys.onEnterPressed: if (!root.running) root.runSpeedTest()
|
||||||
|
|
||||||
ColumnLayout {
|
Item {
|
||||||
id: content
|
id: cluster
|
||||||
anchors.fill: parent
|
anchors.centerIn: parent
|
||||||
spacing: Style.space(16)
|
width: content.implicitWidth
|
||||||
|
height: content.implicitHeight
|
||||||
|
// Narrow or heavily scaled outputs: shrink the whole cluster rather
|
||||||
|
// than clipping it at the screen edge.
|
||||||
|
scale: Math.min(1,
|
||||||
|
(keyCatcher.width - Style.space(32)) / Math.max(1, width),
|
||||||
|
(keyCatcher.height - Style.space(32)) / Math.max(1, height))
|
||||||
|
|
||||||
Text {
|
// Swallow clicks so only the scrim outside the cluster dismisses.
|
||||||
visible: root.connectionName !== ""
|
MouseArea { anchors.fill: parent; onClicked: {} }
|
||||||
text: root.connectionName.toUpperCase()
|
|
||||||
color: root.onScrimDim
|
|
||||||
font.family: root.bar.fontFamily
|
|
||||||
font.pixelSize: Style.font.caption
|
|
||||||
font.bold: true
|
|
||||||
font.letterSpacing: 2
|
|
||||||
Layout.fillWidth: true
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
ColumnLayout {
|
||||||
spacing: Style.space(48)
|
id: content
|
||||||
Layout.alignment: Qt.AlignHCenter
|
anchors.fill: parent
|
||||||
|
spacing: Style.space(16)
|
||||||
|
|
||||||
SpeedDial {
|
Text {
|
||||||
id: downDial
|
visible: root.connectionName !== ""
|
||||||
label: "DOWNLOAD"
|
text: root.connectionName.toUpperCase()
|
||||||
value: root.downloadValue
|
color: root.onScrimDim
|
||||||
live: root.running && root.phase === "down"
|
font.family: root.fontFamily
|
||||||
|
font.pixelSize: Style.font.caption
|
||||||
|
font.bold: true
|
||||||
|
font.letterSpacing: 2
|
||||||
|
Layout.fillWidth: true
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
SpeedDial {
|
Row {
|
||||||
id: upDial
|
spacing: Style.space(48)
|
||||||
label: "UPLOAD"
|
Layout.alignment: Qt.AlignHCenter
|
||||||
value: root.uploadValue
|
|
||||||
live: root.running && root.phase === "up"
|
SpeedDial {
|
||||||
|
id: downDial
|
||||||
|
label: "DOWNLOAD"
|
||||||
|
value: root.downloadValue
|
||||||
|
live: root.running && root.phase === "down"
|
||||||
|
}
|
||||||
|
|
||||||
|
SpeedDial {
|
||||||
|
id: upDial
|
||||||
|
label: "UPLOAD"
|
||||||
|
value: root.uploadValue
|
||||||
|
live: root.running && root.phase === "up"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Centered on the dial pair. Fades rather than unmounts while a run
|
// Centered on the dial pair. Fades rather than unmounts while a run
|
||||||
// is in flight, so the cluster never shifts.
|
// is in flight, so the cluster never shifts.
|
||||||
Button {
|
Button {
|
||||||
text: "Run Again"
|
text: "Run Again"
|
||||||
tooltipText: "Measure again via fast.com"
|
tooltipText: "Measure again via fast.com"
|
||||||
bordered: true
|
bordered: true
|
||||||
enabled: !root.running
|
enabled: !root.running
|
||||||
opacity: root.running ? 0 : 1
|
opacity: root.running ? 0 : 1
|
||||||
foreground: root.onScrim
|
foreground: root.onScrim
|
||||||
fontFamily: root.bar.fontFamily
|
fontFamily: root.fontFamily
|
||||||
fontSize: Style.font.bodySmall
|
fontSize: Style.font.bodySmall
|
||||||
horizontalPadding: Style.space(14)
|
horizontalPadding: Style.space(14)
|
||||||
verticalPadding: Style.space(4)
|
verticalPadding: Style.space(4)
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
onClicked: root.runAgainRequested()
|
onClicked: root.runSpeedTest()
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
NumberAnimation { duration: 240; easing.type: Easing.OutCubic }
|
NumberAnimation { duration: 240; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
visible: root.failed
|
visible: root.failed
|
||||||
text: root.error
|
text: root.error
|
||||||
color: root.onScrimUrgent
|
color: root.onScrimUrgent
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.fontFamily
|
||||||
font.pixelSize: Style.font.bodySmall
|
font.pixelSize: Style.font.bodySmall
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.maximumWidth: Style.space(440)
|
Layout.maximumWidth: Style.space(440)
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -368,7 +516,7 @@ PanelWindow {
|
|||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
text: dial.reading < 10 ? dial.reading.toFixed(1) : Math.round(dial.reading).toString()
|
text: dial.reading < 10 ? dial.reading.toFixed(1) : Math.round(dial.reading).toString()
|
||||||
color: root.onScrim
|
color: root.onScrim
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.fontFamily
|
||||||
font.pixelSize: Style.font.display
|
font.pixelSize: Style.font.display
|
||||||
font.bold: true
|
font.bold: true
|
||||||
}
|
}
|
||||||
@@ -377,7 +525,7 @@ PanelWindow {
|
|||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
text: "Mbps"
|
text: "Mbps"
|
||||||
color: root.onScrimDim
|
color: root.onScrimDim
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.fontFamily
|
||||||
font.pixelSize: Style.font.caption
|
font.pixelSize: Style.font.caption
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -389,7 +537,7 @@ PanelWindow {
|
|||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
text: dial.label
|
text: dial.label
|
||||||
color: root.onScrimDim
|
color: root.onScrimDim
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.fontFamily
|
||||||
font.pixelSize: Style.font.caption
|
font.pixelSize: Style.font.caption
|
||||||
font.bold: true
|
font.bold: true
|
||||||
font.letterSpacing: 1.5
|
font.letterSpacing: 1.5
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"id": "omarchy.speedtest",
|
||||||
|
"name": "Speed Test",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Omarchy",
|
||||||
|
"description": "Internet speed test with download and upload dials",
|
||||||
|
"kinds": [
|
||||||
|
"panel"
|
||||||
|
],
|
||||||
|
"entryPoints": {
|
||||||
|
"panel": "Panel.qml"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user