Skip to content
📖 14 min🎯Difficulty:
Prerequisites:CPU widgetRAM widgetDisk widgetNetwork widget
You'll build:A full-screen dashboard showing system performance, weather, and calendar

The Problem

Users often want a glanceable overview of their system — CPU load, memory usage, disk space, network activity, weather, and upcoming events. Opening multiple widgets or terminals to check these individually is inefficient.

The Naive Approach

Place every widget inline in a panel:

qml
Row {
  CpuWidget {}
  RamWidget {}
  DiskWidget {}
  NetworkWidget {}
}

This works but the panel becomes overcrowded. Users can't see detailed information like per-core CPU usage or process lists without clicking through to separate windows.

💡Mental Model

Think of a dashboard as the instrument cluster in a car. It consolidates all critical information into one organized view: speed (CPU), fuel (RAM), odometer (disk), navigation (network), and climate (weather). Each gauge is specialized but shares the same display space.

The Idea

A dashboard is a large popup or fullscreen window containing multiple data panels. Each panel is a self-contained component that can be developed independently. The dashboard exposes configuration options for which panels to show and their arrangement.

Let's Build It

qml
import Quickshell
import Quickshell.Services
import QtQml.Models

PopupWindow {
  id: root
  width: 800
  height: 600
  visible: false

  property var cpuData: []
  property var memoryData: {}
  property var diskData: []

  // Update timer
  Timer {
    interval: 2000
    running: root.visible
    repeat: true
    onTriggered: refreshData()
  }

  function refreshData() {
    // In production, fetch from system services
    cpuData = [45, 62, 30, 78, 55, 41, 82, 33] // per-core percentages
    memoryData = { used: 8.2, total: 16, percent: 51 }
    diskData = [
      { mount: "/", used: 120, total: 500, percent: 24 },
      { mount: "/home", used: 340, total: 1000, percent: 34 }
    ]
  }

  Rectangle {
    anchors.fill: parent; color: "#1e1e2e"; radius: 8

    Grid {
      anchors.fill: parent; anchors.margins: 16
      columns: 2; spacing: 12

      // CPU Panel
      Rectangle {
        width: 370; height: 160; radius: 8; color: "#181825"
        Column {
          anchors { fill: parent; margins: 12 }; spacing: 8
          Text { text: "CPU"; font.pixelSize: 14; font.bold: true; color: "#cdd6f4" }

          Row {
            spacing: 4
            Repeater {
              model: root.cpuData
              delegate: Rectangle {
                required property var modelData
                width: 40; height: 80; radius: 3; color: "#313244"
                Rectangle {
                  anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter }
                  width: 36; height: parent.height * modelData / 100; radius: 2
                  color: modelData > 80 ? "#f38ba8" : modelData > 50 ? "#fab387" : "#a6e3a1"
                }
                Text {
                  anchors { bottom: parent.top; horizontalCenter: parent.horizontalCenter; bottomMargin: 4 }
                  text: modelData + "%"; color: "#6c7086"; font.pixelSize: 9
                }
              }
            }
          }
        }
      }

      // Memory Panel
      Rectangle {
        width: 370; height: 160; radius: 8; color: "#181825"
        Column {
          anchors { fill: parent; margins: 12 }; spacing: 8
          Text { text: "Memory"; font.pixelSize: 14; font.bold: true; color: "#cdd6f4" }
          Text { text: memoryData.used + " / " + memoryData.total + " GB"; font.pixelSize: 24; font.bold: true; color: "#89b4fa" }
          Rectangle { width: parent.width; height: 8; radius: 4; color: "#313244"
            Rectangle { width: parent.width * memoryData.percent / 100; height: 8; radius: 4; color: "#89b4fa" }
          }
        }
      }

      // Disk Panel
      Rectangle {
        width: 370; height: 160; radius: 8; color: "#181825"
        Column {
          anchors { fill: parent; margins: 12 }; spacing: 8
          Text { text: "Storage"; font.pixelSize: 14; font.bold: true; color: "#cdd6f4" }
          Repeater {
            model: root.diskData
            delegate: Rectangle {
              required property var modelData
              width: parent.width; height: 40; color: "transparent"
              Text { text: modelData.mount + ": " + modelData.used + "/" + modelData.total + " GB"; font.pixelSize: 12; color: "#a6adc8" }
              Rectangle { y: 20; width: parent.width; height: 6; radius: 3; color: "#313244"
                Rectangle { width: parent.width * modelData.percent / 100; height: 6; radius: 3; color: "#a6e3a1" }
              }
            }
          }
        }
      }

      // Process List
      Rectangle {
        width: 370; height: 160; radius: 8; color: "#181825"
        Column {
          anchors { fill: parent; margins: 12 }; spacing: 4
          Text { text: "Top Processes"; font.pixelSize: 14; font.bold: true; color: "#cdd6f4" }
          // Populated via Qt.process(["ps", "aux", "--sort=-%cpu"])
        }
      }
    }
  }
}
🛠️Let's Build It

The dashboard uses a Grid layout to arrange monitoring panels. Each panel is a self-contained component that can be pulled out and reused. A Timer refreshes data every 2 seconds while visible.

Let's Improve It

Add configuration options and panel rearrangement:

qml
property var enabledPanels: ["cpu", "memory", "disk", "network", "weather", "calendar"]

// Filter panels based on config
Repeater {
  model: enabledPanels
  delegate: Loader {
    source: panelName + "-panel.qml"
    visible: modelData in enabledPanels
  }
}
⚠️Common Mistake

Updating too frequently. Refreshing CPU/memory/disk every 100ms wastes CPU showing the CPU usage. 1-2 second intervals are sufficient for monitoring. Use longer intervals (30-60s) for weather and disk space, which change slowly.

Blocking the UI thread during data collection. If your dashboard collects data by spawning shell commands (ps aux, df -h), the synchronous wait blocks the UI. Use Qt.process() with async callbacks and show loading indicators.

Not handling removable drives. Disk monitoring breaks if a USB drive is unplugged. Always handle null or undefined disk entries gracefully — show "No data" instead of a crash.

Under the Hood

A dashboard is a composition pattern: it aggregates data from multiple independent sources (CPU via /proc/stat, memory via /proc/meminfo, disk via statfs(), network via /proc/net/dev) and presents them in a unified view. Each data source is best accessed through the most efficient mechanism: D-Bus for systemd services, procfs for kernel stats, and Quickshell services for shell-level information.

Exercises

Exercise
Add a "Process Killer" to the process list. Each row shows PID, name, and CPU%. Add a "Kill" button that sends SIGTERM (or SIGKILL for stubborn processes).
⭐⭐Exercise
Implement a weather panel. Fetch weather data from `wttr.in` or OpenWeatherMap. Cache the response and refresh every 30 minutes. Show temperature, conditions, and 5-day forecast.
⭐⭐⭐Exercise
Build a dashboard plugin system. Allow users to write their own panels as separate QML files dropped into `~/.config/quickshell/dashboard/panels/`. Load them dynamically with `Loader` and register them in a configuration menu.
What You've Learned
  • Dashboard aggregates system metrics into a unified glanceable view
  • Use Timer with appropriate intervals (1-2s for CPU/memory, 30-60s for weather)
  • Each panel is an independent component that can be developed separately
  • Avoid blocking the UI thread during data collection

Built with VitePress. Licensed under MIT.