Refine network panel connection details

This commit is contained in:
David Heinemeier Hansson
2026-05-22 10:55:42 +02:00
parent 590921d397
commit e9eeb0bdfc
+106 -130
View File
@@ -30,8 +30,8 @@ Panel {
property string prevIface: "" property string prevIface: ""
property real downloadRate: 0 // bytes/sec property real downloadRate: 0 // bytes/sec
property real uploadRate: 0 // bytes/sec property real uploadRate: 0 // bytes/sec
property int ethernetPhraseIndex: 0 property int connectionPhraseIndex: 0
readonly property var ethernetPhrases: [ readonly property var connectionPhrases: [
"Wiring bits", "Wiring bits",
"Handling packets", "Handling packets",
"Sorting frames", "Sorting frames",
@@ -40,7 +40,7 @@ Panel {
"Counting collisions", "Counting collisions",
"Bending light", "Bending light",
] ]
readonly property string ethernetPhrase: ethernetPhrases[ethernetPhraseIndex % ethernetPhrases.length] readonly property string connectionPhrase: connectionPhrases[connectionPhraseIndex % connectionPhrases.length]
readonly property bool networkManagerAvailable: Networking.backend === NetworkBackendType.NetworkManager readonly property bool networkManagerAvailable: Networking.backend === NetworkBackendType.NetworkManager
readonly property var networkDevices: Networking.devices ? Networking.devices.values : [] readonly property var networkDevices: Networking.devices ? Networking.devices.values : []
readonly property var wifiDevice: findDevice(DeviceType.Wifi) readonly property var wifiDevice: findDevice(DeviceType.Wifi)
@@ -82,11 +82,8 @@ Panel {
property string focusSection: "dns" // "header" | "dns" | "wifi" property string focusSection: "dns" // "header" | "dns" | "wifi"
property int headerIndex: 0 property int headerIndex: 0
readonly property bool canDisconnect: !!connectedWifiNetwork readonly property bool canDisconnect: !!connectedWifiNetwork
// The disconnect button is suppressed on ethernet (see disconnectBtn.visible). readonly property bool headerHasDisconnect: false
// headerActionCount/nav must agree, so the keyboard cursor never lands on a readonly property int headerActionCount: 0
// hidden button.
readonly property bool headerHasDisconnect: canDisconnect && info.type !== "ethernet"
readonly property int headerActionCount: headerHasDisconnect ? 1 : 0
readonly property var dnsProviders: ["DHCP", "Cloudflare", "Google", "Custom"] readonly property var dnsProviders: ["DHCP", "Cloudflare", "Google", "Custom"]
property int dnsIndex: 0 property int dnsIndex: 0
@@ -272,17 +269,24 @@ Panel {
syncWifiNetworks() syncWifiNetworks()
} }
function formatSpeed(mbps) { function formatHeaderSpeed(mbps) {
var v = parseInt(mbps, 10) var v = parseInt(mbps, 10)
if (!v || v < 0) return "" if (!v || v < 0) return ""
if (v >= 1000) return (v / 1000).toFixed(v % 1000 === 0 ? 0 : 1) + " Gbps" if (v >= 1000) return (v / 1000).toFixed(v % 1000 === 0 ? 0 : 1) + "gbit"
return v + " Mbps" return v + "mbit"
} }
function formatFreq(mhz) { function formatHeaderFreq(mhz) {
var v = parseFloat(mhz) var v = parseFloat(mhz)
if (!v) return "" if (!v) return ""
return (v / 1000).toFixed(1) + " GHz" var ghz = v / 1000
return ghz.toFixed(ghz % 1 === 0 ? 0 : 1) + "ghz"
}
function headerDetail() {
if (info.type === "ethernet") return formatHeaderSpeed(info.speed || "")
if (info.type === "wifi") return formatHeaderFreq(info.freq || "")
return ""
} }
function updateDetails(raw) { function updateDetails(raw) {
@@ -585,21 +589,21 @@ Panel {
} }
Timer { Timer {
id: ethernetPhraseTimer id: connectionPhraseTimer
interval: 2800 interval: 2800
running: root.opened && root.info.type === "ethernet" running: root.opened && (root.info.type === "ethernet" || (root.info.type === "wifi" && root.canDisconnect))
repeat: true repeat: true
onTriggered: ethernetPhraseSwap.restart() onTriggered: connectionPhraseSwap.restart()
} }
SequentialAnimation { SequentialAnimation {
id: ethernetPhraseSwap id: connectionPhraseSwap
PropertyAnimation { PropertyAnimation {
target: heroMeta; property: "opacity" target: heroMeta; property: "opacity"
to: 0.0; duration: 180; easing.type: Easing.OutQuad to: 0.0; duration: 180; easing.type: Easing.OutQuad
} }
ScriptAction { ScriptAction {
script: root.ethernetPhraseIndex = (root.ethernetPhraseIndex + 1) % root.ethernetPhrases.length script: root.connectionPhraseIndex = (root.connectionPhraseIndex + 1) % root.connectionPhrases.length
} }
PropertyAnimation { PropertyAnimation {
target: heroMeta; property: "opacity" target: heroMeta; property: "opacity"
@@ -610,8 +614,8 @@ Panel {
Connections { Connections {
target: root target: root
function onInfoChanged() { function onInfoChanged() {
if (root.info.type !== "ethernet") { if (!(root.info.type === "ethernet" || (root.info.type === "wifi" && root.canDisconnect))) {
ethernetPhraseSwap.stop() connectionPhraseSwap.stop()
heroMeta.opacity = 1.0 heroMeta.opacity = 1.0
} }
} }
@@ -734,7 +738,7 @@ Panel {
// ---------- Hero: network icon · SSID + state · actions ---------- // ---------- Hero: network icon · SSID + state · actions ----------
Item { Item {
width: parent.width width: parent.width
implicitHeight: Math.max(heroIcon.implicitHeight, heroLabels.implicitHeight, headerActions.implicitHeight) implicitHeight: Math.max(heroIcon.implicitHeight, heroLabels.implicitHeight)
Text { Text {
id: heroIcon id: heroIcon
@@ -769,24 +773,54 @@ Panel {
id: heroLabels id: heroLabels
anchors.left: heroIcon.right anchors.left: heroIcon.right
anchors.leftMargin: Style.space(14) anchors.leftMargin: Style.space(14)
anchors.right: headerActions.left anchors.right: parent.right
anchors.rightMargin: Style.space(10)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Style.space(2) spacing: Style.space(2)
Text { Row {
id: heroSsid id: heroSsid
width: parent.width width: parent.width
text: {
readonly property string title: {
if (root.info.type === "wifi") return root.info.ssid || "Wi-Fi" if (root.info.type === "wifi") return root.info.ssid || "Wi-Fi"
if (root.info.type === "ethernet") return "Ethernet" if (root.info.type === "ethernet") return "Ethernet"
return root.info.iface || (root.kind === "disconnected" ? "Disconnected" : "No connection") return root.info.iface || (root.kind === "disconnected" ? "Disconnected" : "No connection")
} }
color: root.bar.foreground readonly property string detail: root.headerDetail()
font.family: root.bar.fontFamily
font.pixelSize: Style.font.title Text {
font.bold: true text: heroSsid.title
elide: Text.ElideRight width: Math.min(implicitWidth, Math.max(0, parent.width - (heroDetailPill.visible ? heroDetailPill.implicitWidth + Style.space(8) : 0)))
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: Style.font.title
font.bold: true
elide: Text.ElideRight
}
Item { width: Math.max(0, parent.width - parent.children[0].width - heroDetailPill.implicitWidth); height: 1 }
Rectangle {
id: heroDetailPill
visible: heroSsid.detail !== ""
implicitWidth: heroDetail.implicitWidth + Style.space(10)
implicitHeight: heroDetail.implicitHeight + Style.space(4)
anchors.verticalCenter: parent.verticalCenter
color: "transparent"
border.color: Style.normalBorderFor(root.bar.foreground)
border.width: Style.normalBorderWidth
radius: Style.cornerRadius
Text {
id: heroDetail
anchors.centerIn: parent
text: heroSsid.detail
color: Qt.darker(root.bar.foreground, 1.4)
font.family: root.bar.fontFamily
font.pixelSize: Style.font.body
font.bold: true
}
}
} }
Text { Text {
@@ -794,11 +828,11 @@ Panel {
width: parent.width width: parent.width
text: { text: {
if (root.info.type === "wifi") { if (root.info.type === "wifi") {
if (root.canDisconnect) return "CONNECTED" if (root.canDisconnect) return root.connectionPhrase.toUpperCase()
if (root.kind === "disconnected") return "NOT CONNECTED" if (root.kind === "disconnected") return "NOT CONNECTED"
return "" return ""
} }
if (root.info.type === "ethernet") return root.ethernetPhrase.toUpperCase() if (root.info.type === "ethernet") return root.connectionPhrase.toUpperCase()
if (root.kind === "disconnected") return "NOT CONNECTED" if (root.kind === "disconnected") return "NOT CONNECTED"
return "" return ""
} }
@@ -812,115 +846,57 @@ Panel {
} }
} }
Row {
id: headerActions
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Style.space(4)
PanelActionButton {
id: disconnectBtn
// Hide on ethernet — the panel can't disconnect a wired link, and
// a wifi-off glyph next to the ethernet hero icon reads as a state
// contradiction.
visible: root.canDisconnect && root.info.type !== "ethernet"
enabled: !root.busy
hasCursor: root.cursorActive && root.focusSection === "header" && root.headerIndex === 0
iconText: "󰖪"
fontSize: Style.font.heading
size: Style.space(30)
tooltipText: "Disconnect"
foreground: root.bar.foreground
hoverColor: root.bar.urgent
fontFamily: root.bar.fontFamily
anchors.verticalCenter: parent.verticalCenter
onHovered: function(h) {
if (!h) return
root.cursorActive = true
root.focusSection = "header"
root.headerIndex = 0
}
onClicked: root.disconnect(root.connectedWifiNetwork)
}
}
} }
// Connection details: IP, gateway, link speed, etc. Two equal-width // Connection details: transfer metrics first, then IP/Gateway.
// columns spanning the panel — matches the Power panel's stats grid. Column {
Row {
visible: !!root.info.iface visible: !!root.info.iface
width: parent.width width: parent.width
spacing: Style.space(20) spacing: Style.spacing.labelGap
Column { Row {
width: (parent.width - parent.spacing) / 2 visible: root.info.rx_bytes !== undefined
spacing: Style.spacing.labelGap width: parent.width
InfoPair { spacing: Style.space(20)
visible: !!root.info.ip
label: "IP" Column {
value: root.info.ip || "" width: (parent.width - parent.spacing) / 2
copyable: true spacing: Style.spacing.labelGap
tooltipText: "Copy IP" InfoPair { label: "Receiving"; value: root.formatRate(root.downloadRate) }
InfoPair { label: "Downloaded"; value: root.formatBytes(parseFloat(root.info.rx_bytes || "0")) }
} }
InfoPair {
visible: !!root.info.gateway Column {
label: "Gateway" width: (parent.width - parent.spacing) / 2
value: root.info.gateway || "" spacing: Style.spacing.labelGap
copyable: true InfoPair { label: "Sending"; value: root.formatRate(root.uploadRate) }
tooltipText: "Copy gateway" InfoPair { label: "Uploaded"; value: root.formatBytes(parseFloat(root.info.tx_bytes || "0")) }
} }
} }
Column { Row {
width: (parent.width - parent.spacing) / 2 width: parent.width
spacing: Style.spacing.labelGap spacing: Style.space(20)
Column {
// Ethernet details width: (parent.width - parent.spacing) / 2
InfoPair { InfoPair {
visible: root.info.type === "ethernet" && !!root.info.speed visible: !!root.info.ip
label: "Link" label: "IP Address"
value: root.formatSpeed(root.info.speed || "") value: root.info.ip || ""
copyable: true
tooltipText: "Copy IP"
}
} }
Column {
// Wi-Fi details width: (parent.width - parent.spacing) / 2
InfoPair { InfoPair {
visible: root.info.type === "wifi" && !!root.info.freq visible: !!root.info.gateway
label: "Band" label: "Gateway"
value: root.formatFreq(root.info.freq || "") value: root.info.gateway || ""
copyable: true
tooltipText: "Copy gateway"
}
} }
InfoPair {
visible: root.info.type === "wifi" && !!root.info.bitrate
label: "Link"
value: root.info.bitrate || ""
}
}
}
PanelSeparator {
visible: !!root.info.iface && root.info.rx_bytes !== undefined
foreground: root.bar.foreground
}
// Throughput: instantaneous rate (from sample-to-sample delta) plus
// cumulative bytes since the interface came up.
Row {
visible: !!root.info.iface && root.info.rx_bytes !== undefined
width: parent.width
spacing: Style.space(20)
Column {
width: (parent.width - parent.spacing) / 2
spacing: Style.spacing.labelGap
InfoPair { label: "Receiving"; value: root.formatRate(root.downloadRate) }
InfoPair { label: "Downloaded"; value: root.formatBytes(parseFloat(root.info.rx_bytes || "0")) }
}
Column {
width: (parent.width - parent.spacing) / 2
spacing: Style.spacing.labelGap
InfoPair { label: "Sending"; value: root.formatRate(root.uploadRate) }
InfoPair { label: "Uploaded"; value: root.formatBytes(parseFloat(root.info.tx_bytes || "0")) }
} }
} }