controlcenter: ethernet panel (debug)

This commit is contained in:
ATMDA 2025-11-10 10:00:04 -05:00
parent 47dfcf4baf
commit 1debf488ee
8 changed files with 983 additions and 7 deletions

View file

@ -158,8 +158,13 @@ Item {
NavItem {
Layout.topMargin: Appearance.spacing.large * 2
icon: "network_manage"
label: "network"
icon: "cable"
label: "ethernet"
}
NavItem {
icon: "wifi"
label: "wireless"
}
NavItem {

View file

@ -1,5 +1,6 @@
pragma ComponentBehavior: Bound
import "ethernet"
import "bluetooth"
import "network"
import "audio"
@ -26,27 +27,34 @@ ClippingRectangle {
Pane {
index: 0
sourceComponent: NetworkPane {
sourceComponent: EthernetPane {
session: root.session
}
}
Pane {
index: 1
sourceComponent: BtPane {
sourceComponent: NetworkPane {
session: root.session
}
}
Pane {
index: 2
sourceComponent: AudioPane {
sourceComponent: BtPane {
session: root.session
}
}
Pane {
index: 3
sourceComponent: AudioPane {
session: root.session
}
}
Pane {
index: 4
sourceComponent: AppearancePane {
session: root.session
}

View file

@ -2,7 +2,7 @@ import Quickshell.Bluetooth
import QtQuick
QtObject {
readonly property list<string> panes: ["network", "bluetooth", "audio", "appearance"]
readonly property list<string> panes: ["ethernet", "wireless", "bluetooth", "audio", "appearance"]
required property var root
property bool floating: false
@ -12,6 +12,7 @@ QtObject {
readonly property Bt bt: Bt {}
readonly property Network network: Network {}
readonly property Ethernet ethernet: Ethernet {}
onActiveChanged: activeIndex = panes.indexOf(active)
onActiveIndexChanged: active = panes[activeIndex]
@ -29,4 +30,8 @@ QtObject {
property bool showPasswordDialog: false
property var pendingNetwork
}
component Ethernet: QtObject {
property var active
}
}

View file

@ -0,0 +1,204 @@
pragma ComponentBehavior: Bound
import ".."
import qs.components
import qs.components.controls
import qs.components.effects
import qs.components.containers
import qs.services
import qs.config
import QtQuick
import QtQuick.Layouts
Item {
id: root
required property Session session
readonly property var device: session.ethernet.active
StyledFlickable {
anchors.fill: parent
flickableDirection: Flickable.VerticalFlick
contentHeight: layout.height
ColumnLayout {
id: layout
anchors.left: parent.left
anchors.right: parent.right
spacing: Appearance.spacing.normal
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
animate: true
text: "cable"
font.pointSize: Appearance.font.size.extraLarge * 3
font.bold: true
}
StyledText {
Layout.alignment: Qt.AlignHCenter
animate: true
text: root.device?.interface ?? qsTr("Unknown")
font.pointSize: Appearance.font.size.large
font.bold: true
}
StyledText {
Layout.topMargin: Appearance.spacing.large
text: qsTr("Connection status")
font.pointSize: Appearance.font.size.larger
font.weight: 500
}
StyledText {
text: qsTr("Connection settings for this device")
color: Colours.palette.m3outline
}
StyledRect {
Layout.fillWidth: true
implicitHeight: deviceStatus.implicitHeight + Appearance.padding.large * 2
radius: Appearance.rounding.normal
color: Colours.tPalette.m3surfaceContainer
ColumnLayout {
id: deviceStatus
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.larger
Toggle {
label: qsTr("Connected")
checked: root.device?.connected ?? false
toggle.onToggled: {
if (checked) {
if (root.device?.connection) {
Network.connectEthernet(root.device.connection);
}
} else {
if (root.device?.connection) {
Network.disconnectEthernet(root.device.connection);
}
}
}
}
}
}
StyledText {
Layout.topMargin: Appearance.spacing.large
text: qsTr("Device properties")
font.pointSize: Appearance.font.size.larger
font.weight: 500
}
StyledText {
text: qsTr("Additional information")
color: Colours.palette.m3outline
}
StyledRect {
Layout.fillWidth: true
implicitHeight: deviceProps.implicitHeight + Appearance.padding.large * 2
radius: Appearance.rounding.normal
color: Colours.tPalette.m3surfaceContainer
ColumnLayout {
id: deviceProps
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.small / 2
StyledText {
text: qsTr("Interface")
}
StyledText {
text: root.device?.interface ?? qsTr("Unknown")
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.small
}
StyledText {
Layout.topMargin: Appearance.spacing.normal
text: qsTr("Connection")
}
StyledText {
text: root.device?.connection || qsTr("Not connected")
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.small
}
StyledText {
Layout.topMargin: Appearance.spacing.normal
text: qsTr("State")
}
StyledText {
text: root.device?.state ?? qsTr("Unknown")
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.small
}
}
}
}
}
component Toggle: RowLayout {
required property string label
property alias checked: toggle.checked
property alias toggle: toggle
Layout.fillWidth: true
spacing: Appearance.spacing.normal
StyledText {
Layout.fillWidth: true
text: parent.label
}
StyledSwitch {
id: toggle
cLayer: 2
}
}
}

View file

@ -0,0 +1,262 @@
pragma ComponentBehavior: Bound
import ".."
import qs.components
import qs.components.controls
import qs.components.containers
import qs.services
import qs.config
import QtQuick
import QtQuick.Layouts
ColumnLayout {
id: root
required property Session session
spacing: Appearance.spacing.small
RowLayout {
spacing: Appearance.spacing.smaller
StyledText {
text: qsTr("Settings")
font.pointSize: Appearance.font.size.large
font.weight: 500
}
Item {
Layout.fillWidth: true
}
ToggleButton {
toggled: !root.session.ethernet.active
icon: "settings"
accent: "Primary"
function onClicked(): void {
if (root.session.ethernet.active)
root.session.ethernet.active = null;
else {
root.session.ethernet.active = view.model.get(0)?.modelData ?? null;
}
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
StyledText {
text: qsTr("Devices (%1)").arg(Network.ethernetDeviceCount || Network.ethernetDevices.length)
font.pointSize: Appearance.font.size.large
font.weight: 500
}
}
StyledText {
text: qsTr("All available ethernet devices")
color: Colours.palette.m3outline
}
StyledListView {
id: view
Layout.fillWidth: true
Layout.fillHeight: true
model: Network.ethernetDevices
spacing: Appearance.spacing.small / 2
clip: true
StyledScrollBar.vertical: StyledScrollBar {
flickable: view
}
delegate: StyledRect {
required property var modelData
anchors.left: parent.left
anchors.right: parent.right
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.ethernet.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0)
radius: Appearance.rounding.normal
border.width: root.session.ethernet.active === modelData ? 1 : 0
border.color: Colours.palette.m3primary
StateLayer {
function onClicked(): void {
root.session.ethernet.active = modelData;
}
}
RowLayout {
id: rowLayout
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.normal
spacing: Appearance.spacing.normal
StyledRect {
implicitWidth: implicitHeight
implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2
radius: Appearance.rounding.normal
color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh
MaterialIcon {
id: icon
anchors.centerIn: parent
text: "cable"
font.pointSize: Appearance.font.size.large
fill: modelData.connected ? 1 : 0
color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
}
}
StyledText {
Layout.fillWidth: true
text: modelData.interface || qsTr("Unknown")
}
StyledText {
text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected")
color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline
font.pointSize: Appearance.font.size.small
font.weight: modelData.connected ? 500 : 400
}
StyledRect {
implicitWidth: implicitHeight
implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2
radius: Appearance.rounding.full
color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0)
StateLayer {
function onClicked(): void {
if (modelData.connected && modelData.connection) {
Network.disconnectEthernet(modelData.connection);
} else if (modelData.connection) {
Network.connectEthernet(modelData.connection);
}
}
}
MaterialIcon {
id: connectIcon
anchors.centerIn: parent
text: modelData.connected ? "link_off" : "link"
color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
}
}
}
implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2
}
}
component ToggleButton: StyledRect {
id: toggleBtn
required property bool toggled
property string icon
property string label
property string accent: "Secondary"
function onClicked(): void {
}
Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0)
implicitWidth: toggleBtnInner.implicitWidth + Appearance.padding.large * 2
implicitHeight: toggleBtnIcon.implicitHeight + Appearance.padding.normal * 2
radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 * Math.min(1, Appearance.rounding.scale)
color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`]
StateLayer {
id: toggleStateLayer
color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`]
function onClicked(): void {
toggleBtn.onClicked();
}
}
RowLayout {
id: toggleBtnInner
anchors.centerIn: parent
spacing: Appearance.spacing.normal
MaterialIcon {
id: toggleBtnIcon
visible: !!text
fill: toggleBtn.toggled ? 1 : 0
text: toggleBtn.icon
color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`]
font.pointSize: Appearance.font.size.large
Behavior on fill {
Anim {}
}
}
Loader {
asynchronous: true
active: !!toggleBtn.label
visible: active
sourceComponent: StyledText {
text: toggleBtn.label
color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`]
}
}
}
Behavior on radius {
Anim {
duration: Appearance.anim.durations.expressiveFastSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
}
}
Behavior on Layout.preferredWidth {
Anim {
duration: Appearance.anim.durations.expressiveFastSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
}
}
}
}

View file

@ -0,0 +1,156 @@
pragma ComponentBehavior: Bound
import ".."
import qs.components
import qs.components.effects
import qs.components.containers
import qs.config
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
RowLayout {
id: root
required property Session session
anchors.fill: parent
spacing: 0
Item {
Layout.preferredWidth: Math.floor(parent.width * 0.4)
Layout.minimumWidth: 420
Layout.fillHeight: true
EthernetList {
anchors.fill: parent
anchors.margins: Appearance.padding.large + Appearance.padding.normal
anchors.leftMargin: Appearance.padding.large
anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2
session: root.session
}
InnerBorder {
leftThickness: 0
rightThickness: Appearance.padding.normal / 2
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
ClippingRectangle {
anchors.fill: parent
anchors.margins: Appearance.padding.normal
anchors.leftMargin: 0
anchors.rightMargin: Appearance.padding.normal / 2
radius: rightBorder.innerRadius
color: "transparent"
Loader {
id: loader
property var pane: root.session.ethernet.active
property string paneId: pane ? (pane.interface || "") : ""
anchors.fill: parent
anchors.margins: Appearance.padding.large * 2
opacity: 1
scale: 1
transformOrigin: Item.Center
clip: false
asynchronous: true
sourceComponent: pane ? details : settings
Behavior on paneId {
SequentialAnimation {
ParallelAnimation {
Anim {
target: loader
property: "opacity"
to: 0
easing.bezierCurve: Appearance.anim.curves.standardAccel
}
Anim {
target: loader
property: "scale"
to: 0.8
easing.bezierCurve: Appearance.anim.curves.standardAccel
}
}
PropertyAction {}
ParallelAnimation {
Anim {
target: loader
property: "opacity"
to: 1
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
Anim {
target: loader
property: "scale"
to: 1
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
}
}
}
onPaneChanged: {
paneId = pane ? (pane.interface || "") : "";
}
}
}
InnerBorder {
id: rightBorder
leftThickness: Appearance.padding.normal / 2
}
Component {
id: settings
StyledFlickable {
flickableDirection: Flickable.VerticalFlick
contentHeight: settingsInner.height
EthernetSettings {
id: settingsInner
anchors.left: parent.left
anchors.right: parent.right
session: root.session
}
}
}
Component {
id: details
EthernetDetails {
session: root.session
}
}
}
component Anim: NumberAnimation {
target: loader
duration: Appearance.anim.durations.normal / 2
easing.type: Easing.BezierSpline
}
}

View file

@ -0,0 +1,155 @@
pragma ComponentBehavior: Bound
import ".."
import qs.components
import qs.components.controls
import qs.components.effects
import qs.services
import qs.config
import QtQuick
import QtQuick.Layouts
ColumnLayout {
id: root
required property Session session
spacing: Appearance.spacing.normal
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
text: "cable"
font.pointSize: Appearance.font.size.extraLarge * 3
font.bold: true
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Ethernet settings")
font.pointSize: Appearance.font.size.large
font.bold: true
}
StyledText {
Layout.topMargin: Appearance.spacing.large
text: qsTr("Ethernet devices")
font.pointSize: Appearance.font.size.larger
font.weight: 500
}
StyledText {
text: qsTr("Available ethernet devices")
color: Colours.palette.m3outline
}
StyledRect {
Layout.fillWidth: true
implicitHeight: ethernetInfo.implicitHeight + Appearance.padding.large * 2
radius: Appearance.rounding.normal
color: Colours.tPalette.m3surfaceContainer
ColumnLayout {
id: ethernetInfo
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.small / 2
StyledText {
text: qsTr("Total devices")
}
StyledText {
text: qsTr("%1").arg(Network.ethernetDeviceCount || Network.ethernetDevices.length)
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.small
}
StyledText {
Layout.topMargin: Appearance.spacing.normal
text: qsTr("Connected devices")
}
StyledText {
text: qsTr("%1").arg(Network.ethernetDevices.filter(d => d.connected).length)
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.small
}
}
}
StyledText {
Layout.topMargin: Appearance.spacing.large
text: qsTr("Debug Info")
font.pointSize: Appearance.font.size.larger
font.weight: 500
}
StyledRect {
Layout.fillWidth: true
implicitHeight: debugInfo.implicitHeight + Appearance.padding.large * 2
radius: Appearance.rounding.normal
color: Colours.tPalette.m3surfaceContainer
ColumnLayout {
id: debugInfo
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.small / 2
StyledText {
text: qsTr("Process running: %1").arg(Network.ethernetProcessRunning ? "Yes" : "No")
font.pointSize: Appearance.font.size.small
}
StyledText {
text: qsTr("List length: %1").arg(Network.ethernetDevices.length)
font.pointSize: Appearance.font.size.small
}
StyledText {
text: qsTr("Device count: %1").arg(Network.ethernetDeviceCount)
font.pointSize: Appearance.font.size.small
}
StyledText {
Layout.topMargin: Appearance.spacing.normal
text: qsTr("Debug: %1").arg(Network.ethernetDebugInfo || "No info")
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3outline
wrapMode: Text.Wrap
Layout.maximumWidth: parent.width
}
}
}
}

View file

@ -7,11 +7,24 @@ import QtQuick
Singleton {
id: root
Component.onCompleted: {
// Trigger ethernet device detection after initialization
Qt.callLater(() => {
getEthernetDevices();
});
}
readonly property list<AccessPoint> networks: []
readonly property AccessPoint active: networks.find(n => n.active) ?? null
property bool wifiEnabled: true
readonly property bool scanning: rescanProc.running
property list<var> ethernetDevices: []
readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null
property int ethernetDeviceCount: 0
property string ethernetDebugInfo: ""
property bool ethernetProcessRunning: false
function enableWifi(enabled: bool): void {
const cmd = enabled ? "on" : "off";
enableWifiProc.exec(["nmcli", "radio", "wifi", cmd]);
@ -70,11 +83,27 @@ Singleton {
wifiStatusProc.running = true;
}
function getEthernetDevices(): void {
getEthernetDevicesProc.running = true;
}
function connectEthernet(connectionName: string): void {
connectEthernetProc.exec(["nmcli", "connection", "up", connectionName]);
}
function disconnectEthernet(connectionName: string): void {
disconnectEthernetProc.exec(["nmcli", "connection", "down", connectionName]);
}
Process {
running: true
command: ["nmcli", "m"]
stdout: SplitParser {
onRead: getNetworks.running = true
onRead: {
getNetworks.running = true;
getEthernetDevices();
}
}
}
@ -336,6 +365,158 @@ Singleton {
}
}
Process {
id: getEthernetDevicesProc
running: false
command: ["nmcli", "-g", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"]
environment: ({
LANG: "C.UTF-8",
LC_ALL: "C.UTF-8"
})
onRunningChanged: {
root.ethernetProcessRunning = running;
if (!running) {
// Process finished, update debug info
Qt.callLater(() => {
if (root.ethernetDebugInfo === "" || root.ethernetDebugInfo.includes("Process exited")) {
root.ethernetDebugInfo = "Process finished, waiting for output...";
}
});
}
}
onExited: {
Qt.callLater(() => {
const outputLength = ethernetStdout.text ? ethernetStdout.text.length : 0;
root.ethernetDebugInfo = "Process exited with code: " + exitCode + ", output length: " + outputLength;
if (outputLength > 0) {
// Output was captured, process it
const output = ethernetStdout.text.trim();
root.ethernetDebugInfo = "Processing output from onExited, length: " + output.length + "\nOutput: " + output.substring(0, 200);
root.processEthernetOutput(output);
} else {
root.ethernetDebugInfo = "No output captured in onExited";
}
});
}
stdout: StdioCollector {
id: ethernetStdout
onStreamFinished: {
const output = text.trim();
root.ethernetDebugInfo = "Output received in onStreamFinished! Length: " + output.length + ", First 100 chars: " + output.substring(0, 100);
if (!output || output.length === 0) {
root.ethernetDebugInfo = "No output received (empty)";
return;
}
root.processEthernetOutput(output);
}
}
}
function processEthernetOutput(output: string): void {
const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED";
const rep = new RegExp("\\\\:", "g");
const rep2 = new RegExp(PLACEHOLDER, "g");
const lines = output.split("\n");
root.ethernetDebugInfo = "Processing " + lines.length + " lines";
const allDevices = lines.map(d => {
const dev = d.replace(rep, PLACEHOLDER).split(":");
return {
interface: dev[0]?.replace(rep2, ":") ?? "",
type: dev[1]?.replace(rep2, ":") ?? "",
state: dev[2]?.replace(rep2, ":") ?? "",
connection: dev[3]?.replace(rep2, ":") ?? ""
};
});
root.ethernetDebugInfo = "All devices: " + allDevices.length + ", Types: " + allDevices.map(d => d.type).join(", ");
const ethernetOnly = allDevices.filter(d => d.type === "ethernet");
root.ethernetDebugInfo = "Ethernet devices found: " + ethernetOnly.length;
const ethernetDevices = ethernetOnly.map(d => {
const state = d.state || "";
const connected = state === "100 (connected)" || state === "connected" || state.startsWith("connected");
return {
interface: d.interface,
type: d.type,
state: state,
connection: d.connection,
connected: connected,
ipAddress: "",
gateway: "",
dns: [],
subnet: "",
macAddress: "",
speed: ""
};
});
root.ethernetDebugInfo = "Ethernet devices processed: " + ethernetDevices.length + ", First device: " + (ethernetDevices[0]?.interface || "none");
// Update the list - replace the entire array to ensure QML detects the change
// Create a new array and assign it to the property
const newDevices = [];
for (let i = 0; i < ethernetDevices.length; i++) {
newDevices.push(ethernetDevices[i]);
}
// Replace the entire list
root.ethernetDevices = newDevices;
// Force QML to detect the change by updating a property
root.ethernetDeviceCount = ethernetDevices.length;
// Force QML to re-evaluate the list by accessing it
Qt.callLater(() => {
const count = root.ethernetDevices.length;
root.ethernetDebugInfo = "Final: Found " + ethernetDevices.length + " devices, List length: " + count + ", Parsed all: " + allDevices.length + ", Output length: " + output.length;
});
}
Process {
id: connectEthernetProc
onExited: {
getEthernetDevices();
}
stdout: SplitParser {
onRead: getEthernetDevices()
}
stderr: StdioCollector {
onStreamFinished: {
const error = text.trim();
if (error && error.length > 0 && !error.includes("successfully") && !error.includes("Connection activated")) {
console.warn("Ethernet connection error:", error);
}
}
}
}
Process {
id: disconnectEthernetProc
onExited: {
getEthernetDevices();
}
stdout: SplitParser {
onRead: getEthernetDevices()
}
stderr: StdioCollector {
onStreamFinished: {
const error = text.trim();
if (error && error.length > 0 && !error.includes("successfully") && !error.includes("disconnected")) {
console.warn("Ethernet disconnection error:", error);
}
}
}
}
component AccessPoint: QtObject {
required property var lastIpcObject
readonly property string ssid: lastIpcObject.ssid