pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.modules.common
Singleton {
id: root
// Available interfaces: Map of Types.Network keys to arrays of interface names
property var interfaces: new Map()
property string activeInterface: ""
property int networkType: -1
// Common
property real rateUp: 0.0 // Bytes/s
property real rateDown: 0.0
property var lanIPs: [] // List of IP strings
property string wanIP: ""
// Wireless
property string ssid: ""
property int frequency: 0
property int signalStrength: 0 // dBm
property real bitrateTx: 0.0 // Mbps
property real bitrateRx: 0.0 // Mbps
// Wired
property int linkSpeed: 0 // Mbps
// Internal state for rate calculations
property var tx: 0
property var rx: 0
// Fixed 1000ms timer for rates (not configurable, to ensure /s calc)
Timer {
id: rateTimer
interval: 1000
running: true
repeat: true
triggeredOnStart: true
onTriggered: updateRates()
}
// Configurable timers
Timer {
id: infoTimer
interval: Config.data.network.infoUpdateInterval * 1000
running: true
repeat: true
triggeredOnStart: true
onTriggered: updateInfo()
}
Timer {
id: externalTimer
interval: Config.data.network.externalUpdateInterval * 1000
running: true
repeat: true
triggeredOnStart: true
onTriggered: updateExternal()
}
FileView {
id: txBytesView
path: root.activeInterface ? "/sys/class/net/" + root.activeInterface + "/statistics/tx_bytes" : ""
preload: false
onLoadFailed: (error) => console.log("Network Service: Failed to load tx_bytes:", error)
}
FileView {
id: rxBytesView
path: root.activeInterface ? "/sys/class/net/" + root.activeInterface + "/statistics/rx_bytes" : ""
preload: false
onLoadFailed: (error) => console.log("Network Service: Failed to load rx_bytes:", error)
}
FileView {
id: linkSpeedView
path: root.activeInterface && root.networkType === Types.Network.Wired ? "/sys/class/net/" + root.activeInterface + "/speed" : ""
preload: false
onLoadFailed: (error) => console.log("Network Service: Failed to load link speed:", error)
}
Process {
id: listInterfacesProc
command: [
"find", "/sys/class/net", "-mindepth", "1", "-maxdepth", "1", "-type", "l",
"-exec", "sh", "-c", `n=$(basename {}); [ -d {}/wireless ] || [ -L {}/phy80211 ] && echo "$n wireless" || { [ -d {}/device ] && echo "$n wired" || echo "$n virtual"; }`, `\;`
]
stdout: StdioCollector {
onStreamFinished: {
const lines = this.text.trim().split('\n');
const result = new Map([
[Types.Network.Wired, []],
[Types.Network.Wireless, []],
[Types.Network.Virtual, []],
]);
for (const line of lines) {
const linet = line.trim();
if (linet === '') continue;
const [name, itypeStr] = linet.split(/\s+/);
const itype = Types.stringToNetwork(itypeStr);
result.get(itype).push(name);
}
result.forEach(function(val, key, map) {
map[key] = val.sort();
});
interfaces = result;
}
}
stderr: StdioCollector {
onStreamFinished: {
const stderr = this.text.trim();
if (stderr) {
throw new Error(`Failed running listInterfacesProc. Error: ${stderr}`);
}
}
}
onExited: function(exitCode, exitStatus) {
if (exitCode !== 0) {
throw new Error(`Failed running listInterfacesProc. Exit code: ${exitCode}, exit status: ${exitStatus}`);
}
autoSelectInterface();
if (networkType === Types.Network.Wireless) {
wirelessInfoProc.running = true;
} else if (networkType === Types.Network.Wired){
linkSpeedView.reload();
linkSpeed = parseInt(linkSpeedView.text().trim()) || 0;
}
lanIPProc.running = true;
}
}
Process {
id: lanIPProc
command: ["ip", "-details", "-json", "address", "show", root.activeInterface]
stdout: StdioCollector {
onStreamFinished: {
try {
const data = JSON.parse(this.text);
if (data && data.length > 0) {
lanIPs = data[0].addr_info ? data[0].addr_info.map(info => info.local).filter(ip => ip) : [];
}
} catch (e) {
console.error("Network Service: Failed to parse ip JSON:", e);
lanIPs = [];
}
}
}
stderr: StdioCollector {
onStreamFinished: {
const stderr = this.text.trim();
if (stderr) {
throw new Error(`Failed running lanIPProc. Error: ${stderr}`);
}
}
}
onExited: function(exitCode, exitStatus) {
if (exitCode !== 0) {
throw new Error(`Failed running lanIPProc. Exit code: ${exitCode}, exit status: ${exitStatus}`);
}
}
}
Process {
id: wirelessInfoProc
command: ["iw", "dev", root.activeInterface, "link"]
stdout: StdioCollector {
onStreamFinished: {
const lines = this.text.split('\n');
let ssidFound = "", freqFound = "", rxBitFound = "", txBitFound = "", signalFound = "";
for (let line of lines) {
if (line.includes("SSID:")) ssidFound = line.split("SSID:")[1].trim();
if (line.includes("freq:")) freqFound = line.split("freq:")[1].trim();
if (line.includes("signal:")) signalFound = line.split("signal:")[1].trim().split(' ')[0];
if (line.includes("rx bitrate:")) rxBitFound = line.split("rx bitrate:")[1].trim();
if (line.includes("tx bitrate:")) txBitFound = line.split("tx bitrate:")[1].trim();
}
ssid = ssidFound;
frequency = freqFound ? parseInt(freqFound) : 0;
signalStrength = signalFound ? parseInt(signalFound) : 0;
bitrateRx = rxBitFound ? parseFloat(rxBitFound) : 0;
bitrateTx = txBitFound ? parseFloat(txBitFound) : 0;
}
}
stderr: StdioCollector {
onStreamFinished: {
const stderr = this.text.trim();
if (stderr) {
throw new Error(`Failed running wirelessInfoProc. Error: ${stderr}`);
}
}
}
onExited: function(exitCode, exitStatus) {
if (exitCode !== 0) {
throw new Error(`Failed running wirelessInfoProc. Exit code: ${exitCode}, exit status: ${exitStatus}`);
}
}
}
Process {
id: wanIPProc
// TODO: Use additional sources to improve robustness.
command: ["dig", "+short", "@resolver2.opendns.com", "myip.opendns.com"]
stdout: StdioCollector {
onStreamFinished: wanIP = this.text.trim() || "N/A"
}
stderr: StdioCollector {
onStreamFinished: {
const stderr = this.text.trim();
if (stderr) {
throw new Error(`Failed running wanIPProc. Error: ${stderr}`);
}
}
}
onExited: function(exitCode, exitStatus) {
if (exitCode !== 0) {
throw new Error(`Failed running wanIPProc. Exit code: ${exitCode}, exit status: ${exitStatus}`);
}
}
}
// Public API
function setActiveInterface(interfaceName, networkType) {
root.activeInterface = interfaceName;
root.networkType = networkType;
resetState();
updateInfo();
// Persist the selection
BarState.data.network.activeInterface = interfaceName;
BarState.data.network.type = networkType;
}
// Internal: Auto-select the most appropriate interface, if none is already
// selected. Priority order: persisted state, wireless, wired, loopback.
function autoSelectInterface() {
if (activeInterface) return;
// Wait for the state file to be loaded
BarState.view.waitForJob();
// Try persisted state first
const state = BarState.data.network;
if (state.activeInterface && interfaces.get(state.type)?.includes(state.activeInterface)) {
activeInterface = state.activeInterface;
networkType = state.type;
return;
}
let wireless = interfaces.get(Types.Network.Wireless),
wired = interfaces.get(Types.Network.Wired),
iface, ifaceType;
if (wireless?.length) {
iface = wireless[0];
ifaceType = Types.Network.Wireless;
} else if (wired?.length) {
iface = wired[0];
ifaceType = Types.Network.Wired;
} else {
iface = 'lo';
ifaceType = Types.Network.Virtual;
}
if (iface) {
activeInterface = iface
networkType = ifaceType
}
// Persist the selection
state.activeInterface = iface;
state.type = ifaceType;
}
function updateRates() {
if (!activeInterface) return;
let prevTx = tx, prevRx = rx;
rxBytesView.reload();
rx = parseInt(rxBytesView.text().trim()) || 0;
txBytesView.reload();
tx = parseInt(txBytesView.text().trim()) || 0;
if (prevTx > 0 && prevRx > 0) { // Skip initial
rateUp = Math.max(0, tx - prevTx); // Up = tx (bytes/s)
rateDown = Math.max(0, rx - prevRx); // Down = rx
}
}
function updateInfo() {
listInterfacesProc.running = true;
}
function updateExternal() {
wanIPProc.running = true;
}
function resetState() {
tx = 0; rx = 0; rateUp = 0; rateDown = 0;
lanIPs = []; ssid = ""; frequency = 0; signalStrength = 0;
bitrateTx = 0; bitrateRx = 0; linkSpeed = 0;
}
}