Skip to content

The Problem

A network widget must show Wi-Fi SSID, signal strength, connection state, and maybe IP address. Wired Ethernet should also be detected. The state changes frequently — you connect to a new network, drop signal, or plug in an Ethernet cable.

The Naive Approach

Run iwconfig or nmcli in a polling Timer. These tools have inconsistent output formats and spawning processes every few seconds is wasteful. Worse, polling misses transient states (connecting, authenticating, DHCP timeout).

qml
Timer {
  interval: 5000
  onTriggered: {
    // "nmcli -t -f active,ssid,signal dev wifi" or "iwconfig wlan0"
    // Parse messy output
  }
}
💡Mental Model

Think of network status like a phone's cellular indicator. You don't check your data plan every second — the phone tells you when you connect, disconnect, or switch towers. Similarly, your widget should react to events, not poll for changes.

The Idea

Quickshell provides NetworkManager integration via the NetworkManager singleton. It exposes active connections, device status, signal strength, and SSID through reactive properties. No polling, no shell commands.

Let's Build It

A network widget with Wi-Fi SSID and signal strength:

🛠️Let's Build It
qml
import Quickshell
import Quickshell.Services.NetworkManager

Row {
  spacing: 6

  property var primaryDevice: {
    for (var i = 0; i < NetworkManager.devices.length; i++) {
      var dev = NetworkManager.devices[i]
      if (dev.activeConnection) return dev
    }
    return null
  }

  property var connection: primaryDevice ? primaryDevice.activeConnection : null
  property string ssid: connection ? connection.id : ""
  property int signalStrength: primaryDevice && primaryDevice.type === "wifi"
    ? primaryDevice.wirelessStrength : -1
  property bool connected: connection !== null

  Text {
    text: {
      if (!connected) return ""
      if (primaryDevice && primaryDevice.type === "ethernet") return ""
      if (signalStrength >= 80) return ""
      if (signalStrength >= 50) return ""
      if (signalStrength >= 25) return ""
      return ""
    }
    font.pixelSize: 14
    color: connected ? "#a6e3a1" : "#585b70"
  }

  Text {
    text: ssid
    font.pixelSize: 12
    color: "#cdd6f4"
    visible: ssid !== ""
  }

  Text {
    text: signalStrength >= 0 ? signalStrength + "%" : ""
    font.pixelSize: 10
    color: "#585b70"
    visible: primaryDevice && primaryDevice.type === "wifi"
  }
}

Let's Improve It

Show download/upload speed and IP address:

qml
Row {
  spacing: 8

  property var prevRx: 0
  property var prevTx: 0
  property real speedRx: 0
  property real speedTx: 0

  function updateSpeed() {
    var data = readFile("/sys/class/net/" + iface + "/statistics/rx_bytes")
    var rx = parseInt(data.trim())
    data = readFile("/sys/class/net/" + iface + "/statistics/tx_bytes")
    var tx = parseInt(data.trim())
    speedRx = (rx - prevRx) / 1024 / 2 // KB/s over 2s window
    speedTx = (tx - prevTx) / 1024 / 2
    prevRx = rx
    prevTx = tx
  }

  Text {
    text: " " + speedTx.toFixed(1) + "K"
    font.pixelSize: 10
    color: "#585b70"
  }
  Text {
    text: " " + speedRx.toFixed(1) + "K"
    font.pixelSize: 10
    color: "#585b70"
  }
}
⚠️Common Mistake

Assuming Wi-Fi is the only interface. Desktops use Ethernet. Laptops may have both. Check device.type === "wifi" vs "ethernet". Handle the case where no connection is active.

Polling signal strength. NetworkManager properties are reactive. Just bind to primaryDevice.wirelessStrength — the UI updates when the kernel reports a signal change to NetworkManager.

Exposing raw IP addresses. If you show your local IP in the panel, consider privacy implications for screenshots or streaming. Make it opt-in.

Under the Hood

NetworkManager is a Quickshell module wrapping the org.freedesktop.NetworkManager D-Bus interface. It subscribes to StateChanged, DeviceAdded, DeviceRemoved, and PropertiesChanged signals. The wirelessStrength property updates when NetworkManager receives a scan update from wpa_supplicant (typically every 5-15 seconds).

🔧Under the Hood

Network speed is computed by reading /sys/class/net/&lt;iface&gt;/statistics/rx_bytes and tx_bytes. These are kernel counters incremented by the NIC driver on every packet. By taking deltas over a 2-second window, you get a smoothed throughput rate. The filesystem path must match the active interface name.

Exercises

Exercise
Add a connection type icon that distinguishes Wi-Fi (waves icon), Ethernet (plug icon), and no connection (disconnected icon). Change color based on connectivity state.
⭐⭐Exercise
Build a network list popup: when clicking the widget, show available Wi-Fi networks with SSID and signal strength bars. Indicate which one is currently connected.
⭐⭐⭐Exercise
Implement a data usage tracker per session. Sum rx/tx bytes since the widget loaded and display total transferred in MB/GB. Persist the counter across shell restarts using a JSON file.
What You've Learned
  • NetworkManager singleton provides reactive network state
  • Device type (wifi vs ethernet) determines the icon
  • Signal strength is reactive — no polling needed
  • Network speed comes from sysfs byte counters
  • Always handle the disconnected state gracefully

Built with VitePress. Licensed under MIT.