summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config/BarConfig.qml1
-rw-r--r--modules/bar/popouts/Network.qml219
-rw-r--r--services/Network.qml123
3 files changed, 333 insertions, 10 deletions
diff --git a/config/BarConfig.qml b/config/BarConfig.qml
index 472bd4b..c8a8bba 100644
--- a/config/BarConfig.qml
+++ b/config/BarConfig.qml
@@ -24,5 +24,6 @@ JsonObject {
property int windowPreviewSize: 400
property int trayMenuWidth: 300
property int batteryWidth: 250
+ property int networkWidth: 320
}
}
diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml
index d2b056d..b7e1270 100644
--- a/modules/bar/popouts/Network.qml
+++ b/modules/bar/popouts/Network.qml
@@ -1,22 +1,229 @@
+pragma ComponentBehavior: Bound
+
import qs.widgets
import qs.services
import qs.config
+import qs.utils
+import Quickshell
import QtQuick
+import QtQuick.Layouts
-Column {
+ColumnLayout {
id: root
- spacing: Appearance.spacing.normal
+ property string connectingToSsid: ""
+
+ spacing: Appearance.spacing.small
+ width: Config.bar.sizes.networkWidth
StyledText {
- text: qsTr("Connected to: %1").arg(Network.active?.ssid ?? "None")
+ Layout.topMargin: Appearance.padding.normal
+ Layout.rightMargin: Appearance.padding.small
+ text: qsTr("WiFi %1").arg(Network.wifiEnabled ? "enabled" : "disabled")
+ font.weight: 500
}
- StyledText {
- text: qsTr("Strength: %1/100").arg(Network.active?.strength ?? 0)
+ Toggle {
+ label: qsTr("Enabled")
+ checked: Network.wifiEnabled
+ toggle.onToggled: Network.enableWifi(checked)
}
StyledText {
- text: qsTr("Frequency: %1 MHz").arg(Network.active?.frequency ?? 0)
+ Layout.topMargin: Appearance.spacing.small
+ Layout.rightMargin: Appearance.padding.small
+ text: qsTr("%1 networks available").arg(Network.networks.length)
+ color: Colours.palette.m3onSurfaceVariant
+ font.pointSize: Appearance.font.size.small
+ }
+
+ Repeater {
+ model: ScriptModel {
+ values: [...Network.networks].sort((a, b) => {
+ if (a.active !== b.active)
+ return b.active - a.active;
+ return b.strength - a.strength;
+ }).slice(0, 8)
+ }
+
+ RowLayout {
+ id: networkItem
+
+ required property var modelData
+ readonly property bool isConnecting: root.connectingToSsid === modelData.ssid
+ readonly property bool loading: networkItem.isConnecting
+
+ Layout.fillWidth: true
+ Layout.rightMargin: Appearance.padding.small
+ spacing: Appearance.spacing.small
+
+ opacity: 0
+ scale: 0.7
+
+ Component.onCompleted: {
+ opacity = 1;
+ scale = 1;
+ }
+
+ Behavior on opacity {
+ Anim {}
+ }
+
+ Behavior on scale {
+ Anim {}
+ }
+
+ MaterialIcon {
+ text: Icons.getNetworkIcon(networkItem.modelData.strength)
+ color: networkItem.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
+ }
+
+ MaterialIcon {
+ visible: networkItem.modelData.isSecure
+ text: "lock"
+ font.pointSize: Appearance.font.size.small
+ }
+
+ StyledText {
+ Layout.leftMargin: Appearance.spacing.small / 2
+ Layout.rightMargin: Appearance.spacing.small / 2
+ Layout.fillWidth: true
+ text: networkItem.modelData.ssid
+ elide: Text.ElideRight
+ font.weight: networkItem.modelData.active ? 500 : 400
+ color: networkItem.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurface
+ }
+
+ StyledRect {
+ id: connectBtn
+
+ implicitWidth: implicitHeight
+ implicitHeight: connectIcon.implicitHeight + Appearance.padding.small
+
+ radius: Appearance.rounding.full
+ color: networkItem.modelData.active ? Colours.palette.m3primary : Colours.palette.m3surface
+
+ StyledBusyIndicator {
+ anchors.centerIn: parent
+
+ implicitWidth: implicitHeight
+ implicitHeight: connectIcon.implicitHeight
+
+ running: opacity > 0
+ opacity: networkItem.loading ? 1 : 0
+
+ Behavior on opacity {
+ Anim {}
+ }
+ }
+
+ StateLayer {
+ color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
+ disabled: networkItem.loading || !Network.wifiEnabled
+
+ function onClicked(): void {
+ if (networkItem.modelData.active) {
+ Network.disconnectFromNetwork();
+ } else {
+ root.connectingToSsid = networkItem.modelData.ssid;
+ Network.connectToNetwork(networkItem.modelData.ssid, "");
+ }
+ }
+ }
+
+ MaterialIcon {
+ id: connectIcon
+
+ anchors.centerIn: parent
+ animate: true
+ text: networkItem.modelData.active ? "link_off" : "link"
+ color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
+
+ opacity: networkItem.loading ? 0 : 1
+
+ Behavior on opacity {
+ Anim {}
+ }
+ }
+ }
+ }
+ }
+
+ StyledRect {
+ Layout.topMargin: Appearance.spacing.small
+ Layout.fillWidth: true
+ implicitHeight: rescanBtn.implicitHeight + Appearance.padding.small * 2
+
+ radius: Appearance.rounding.normal
+ color: Network.scanning ? Colours.palette.m3surfaceContainer : Colours.palette.m3primaryContainer
+
+ StateLayer {
+ color: Network.scanning ? Colours.palette.m3onSurface : Colours.palette.m3onPrimaryContainer
+ enabled: !Network.scanning && Network.wifiEnabled
+
+ function onClicked(): void {
+ Network.rescanWifi();
+ }
+ }
+
+ RowLayout {
+ id: rescanBtn
+ anchors.centerIn: parent
+ spacing: Appearance.spacing.small
+
+ MaterialIcon {
+ text: Network.scanning ? "refresh" : "wifi_find"
+ color: Network.scanning ? Colours.palette.m3onSurface : Colours.palette.m3onPrimaryContainer
+
+ RotationAnimation on rotation {
+ running: Network.scanning
+ loops: Animation.Infinite
+ from: 0
+ to: 360
+ duration: 1000
+ }
+ }
+
+ StyledText {
+ text: Network.scanning ? qsTr("Scanning...") : qsTr("Rescan networks")
+ color: Network.scanning ? Colours.palette.m3onSurface : Colours.palette.m3onPrimaryContainer
+ }
+ }
+ }
+
+ // Reset connecting state when network changes
+ Connections {
+ target: Network
+
+ function onActiveChanged(): void {
+ if (Network.active && root.connectingToSsid === Network.active.ssid) {
+ root.connectingToSsid = "";
+ }
+ }
+ }
+
+ component Toggle: RowLayout {
+ required property string label
+ property alias checked: toggle.checked
+ property alias toggle: toggle
+
+ Layout.fillWidth: true
+ Layout.rightMargin: Appearance.padding.small
+ spacing: Appearance.spacing.normal
+
+ StyledText {
+ Layout.fillWidth: true
+ text: parent.label
+ }
+
+ StyledSwitch {
+ id: toggle
+ }
+ }
+
+ component Anim: NumberAnimation {
+ duration: Appearance.anim.durations.normal
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Appearance.anim.curves.standard
}
}
diff --git a/services/Network.qml b/services/Network.qml
index 8515ef5..74b05dc 100644
--- a/services/Network.qml
+++ b/services/Network.qml
@@ -9,9 +9,45 @@ Singleton {
readonly property list<AccessPoint> networks: []
readonly property AccessPoint active: networks.find(n => n.active) ?? null
+ property bool wifiEnabled: true
+ property bool scanning: false
reloadableId: "network"
+ function enableWifi(enabled: bool): void {
+ const cmd = enabled ? "on" : "off";
+ enableWifiProcess.command = ["nmcli", "radio", "wifi", cmd];
+ enableWifiProcess.running = true;
+ }
+
+ function toggleWifi(): void {
+ const cmd = wifiEnabled ? "off" : "on";
+ enableWifiProcess.command = ["nmcli", "radio", "wifi", cmd];
+ enableWifiProcess.running = true;
+ }
+
+ function rescanWifi(): void {
+ scanning = true;
+ rescanProcess.running = true;
+ }
+
+ function connectToNetwork(ssid: string, password: string): void {
+ // TODO: Implement password
+ connectProcess.command = ["nmcli", "conn", "up", ssid];
+ connectProcess.running = true;
+ }
+
+ function disconnectFromNetwork(): void {
+ if (active) {
+ disconnectProcess.command = ["nmcli", "connection", "down", active.ssid];
+ disconnectProcess.running = true;
+ }
+ }
+
+ function getWifiStatus(): void {
+ wifiStatusProcess.running = true;
+ }
+
Process {
running: true
command: ["nmcli", "m"]
@@ -21,9 +57,62 @@ Singleton {
}
Process {
+ id: wifiStatusProcess
+ command: ["nmcli", "radio", "wifi"]
+ environment: ({
+ LANG: "C",
+ LC_ALL: "C"
+ })
+ stdout: StdioCollector {
+ onStreamFinished: {
+ root.wifiEnabled = text.trim() === "enabled";
+ }
+ }
+ Component.onCompleted: running = true
+ }
+
+ Process {
+ id: enableWifiProcess
+ stdout: SplitParser {
+ onRead: {
+ getWifiStatus();
+ getNetworks.running = true;
+ }
+ }
+ }
+
+ Process {
+ id: rescanProcess
+ command: ["nmcli", "dev", "wifi", "list", "--rescan", "yes"]
+ stdout: SplitParser {
+ onRead: {
+ scanning = false;
+ getNetworks.running = true;
+ }
+ }
+ }
+
+ Process {
+ id: connectProcess
+ stdout: SplitParser {
+ onRead: getNetworks.running = true
+ }
+ stderr: SplitParser {
+ onRead: console.warn("Network connection error:", data)
+ }
+ }
+
+ Process {
+ id: disconnectProcess
+ stdout: SplitParser {
+ onRead: getNetworks.running = true
+ }
+ }
+
+ Process {
id: getNetworks
running: true
- command: ["nmcli", "-g", "ACTIVE,SIGNAL,FREQ,SSID,BSSID", "d", "w"]
+ command: ["nmcli", "-g", "ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY", "d", "w"]
environment: ({
LANG: "C",
LC_ALL: "C"
@@ -34,16 +123,40 @@ Singleton {
const rep = new RegExp("\\\\:", "g");
const rep2 = new RegExp(PLACEHOLDER, "g");
- const networks = text.trim().split("\n").map(n => {
+ const allNetworks = text.trim().split("\n").map(n => {
const net = n.replace(rep, PLACEHOLDER).split(":");
return {
active: net[0] === "yes",
strength: parseInt(net[1]),
frequency: parseInt(net[2]),
ssid: net[3],
- bssid: net[4]?.replace(rep2, ":") ?? ""
+ bssid: net[4]?.replace(rep2, ":") ?? "",
+ security: net[5] || ""
};
- });
+ }).filter(n => n.ssid && n.ssid.length > 0);
+
+ // Group networks by SSID and prioritize connected ones
+ const networkMap = new Map();
+ for (const network of allNetworks) {
+ const existing = networkMap.get(network.ssid);
+ if (!existing) {
+ networkMap.set(network.ssid, network);
+ } else {
+ // Prioritize active/connected networks
+ if (network.active && !existing.active) {
+ networkMap.set(network.ssid, network);
+ } else if (!network.active && !existing.active) {
+ // If both are inactive, keep the one with better signal
+ if (network.strength > existing.strength) {
+ networkMap.set(network.ssid, network);
+ }
+ }
+ // If existing is active and new is not, keep existing
+ }
+ }
+
+ const networks = Array.from(networkMap.values());
+
const rNetworks = root.networks;
const destroyed = rNetworks.filter(rn => !networks.find(n => n.frequency === rn.frequency && n.ssid === rn.ssid && n.bssid === rn.bssid));
@@ -71,6 +184,8 @@ Singleton {
readonly property int strength: lastIpcObject.strength
readonly property int frequency: lastIpcObject.frequency
readonly property bool active: lastIpcObject.active
+ readonly property string security: lastIpcObject.security
+ readonly property bool isSecure: security.length > 0
}
Component {