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:
David Heinemeier Hansson
2026-08-07 02:13:31 -07:00
co-authored by Claude Fable 5
parent 4519451655
commit dd2f434a7d
5 changed files with 297 additions and 269 deletions
+1 -1
View File
@@ -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 -151
View File
@@ -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
+2 -2
View File
@@ -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
} }
@@ -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"
}
}