Skip to content

The Problem

Users want to know how much memory is in use — is the browser eating 8GB again? A RAM widget must distinguish between "used" (actively allocated) and "available" (free + cached + buffer memory that can be reclaimed). Showing just "used" vs "total" is misleading because Linux aggressively uses free memory for caching.

The Naive Approach

Read free -h via shell command and parse the output. This spawns a subprocess every time, which is slow and wasteful. Alternatively, parse /proc/meminfo manually with string splitting.

qml
Timer {
  interval: 5000
  onTriggered: {
    // "free -h | grep Mem" and parse the line
    // Or read /proc/meminfo line by line
  }
}
💡Mental Model

Think of RAM like a workbench. Used memory is the project you're actively working on. Cached memory is spare parts stacked in the corner — they take up bench space but can be swept away instantly if you need room. "Available" is the space you'd have after clearing the clutter.

The Idea

Read /proc/meminfo and extract MemTotal, MemAvailable, and MemFree. Compute used as MemTotal - MemAvailable (not MemTotal - MemFree). This gives the true usable memory that can't be reclaimed.

Let's Build It

A RAM usage widget with a progress bar:

🛠️Let's Build It
qml
import Quickshell
import QtQuick

Row {
  spacing: 8

  property real memTotal: 0
  property real memAvailable: 0
  property real usedPercent: 0

  function updateMem() {
    var data = readFile("/proc/meminfo")
    var total = parseMemValue(data, "MemTotal:")
    var available = parseMemValue(data, "MemAvailable:")
    if (total > 0) {
      memTotal = total
      memAvailable = available
      usedPercent = Math.round((total - available) / total * 100)
    }
  }

  function parseMemValue(text, key) {
    var idx = text.indexOf(key)
    if (idx < 0) return 0
    var line = text.substring(idx).split("\n")[0]
    return parseInt(line.replace(/[^0-9]/g, ""))
  }

  Timer {
    interval: 5000
    running: true
    repeat: true
    triggeredOnStart: true
    onTriggered: updateMem()
  }

  Text {
    text: ""
    font.pixelSize: 14
    color: usedPercent > 85 ? "#f38ba8" : "#89b4fa"
  }

  Rectangle {
    width: 60; height: 8
    radius: 4
    color: "#313244"
    anchors.verticalCenter: parent.verticalCenter

    Rectangle {
      width: parent.width * usedPercent / 100
      height: parent.height
      radius: 4
      color: usedPercent > 85 ? "#f38ba8" : usedPercent > 70 ? "#fab387" : "#a6e3a1"
      Behavior on width { SmoothedAnimation { duration: 200 } }
    }
  }

  Text {
    text: Math.round((memTotal - memAvailable) / 1024) + "M / " + Math.round(memTotal / 1024) + "M"
    font.pixelSize: 11
    color: "#a6adc8"
  }
}

Let's Improve It

Add a swap indicator and a memory-hungry process popup:

qml
// Extend with swap info
property real swapTotal: 0
property real swapUsed: 0
property real swapPercent: 0

function updateMem() {
  var data = readFile("/proc/meminfo")
  memTotal = parseMemValue(data, "MemTotal:")
  memAvailable = parseMemValue(data, "MemAvailable:")
  swapTotal = parseMemValue(data, "SwapTotal:")
  var swapFree = parseMemValue(data, "SwapFree:")
  swapUsed = swapTotal - swapFree
  swapPercent = swapTotal > 0 ? Math.round(swapUsed / swapTotal * 100) : 0
  usedPercent = Math.round((memTotal - memAvailable) / memTotal * 100)
}
⚠️Common Mistake

Using MemFree instead of MemAvailable. MemFree is just completely unused pages. MemAvailable includes reclaimable cache and buffers. The former makes memory look nearly full when it's actually healthy.

Formatting units inconsistently. /proc/meminfo reports in kilobytes. Divide by 1024 for MB, or 1048576 for GB. Always store values in KB and convert only for display.

Updating too fast. RAM usage changes slowly. Every 5 seconds is more than enough. Updating every second wastes CPU for no visible benefit.

Under the Hood

/proc/meminfo is generated by the kernel's memory manager. MemAvailable was added in Linux 3.14 and estimates reclaimable memory by walking the LRU lists. Reading the file is cheap — the kernel serializes the numbers into a buffer in microseconds. Quickshell's readFile() slurps the whole file into a string.

🔧Under the Hood

The SmoothedAnimation on the progress bar width ensures smooth visual updates even though the data updates only every 5 seconds. The Behavior on width interpolates between the old and new values over 200ms, preventing jarring jumps.

Exercises

Exercise
Change the progress bar color to a gradient that shifts from green to yellow to red as usage increases from 0 to 100%.
⭐⭐Exercise
Add a breakdown: show `buff/cache` usage as a second segment in the bar (lighter color). Distinguish active vs reclaimable memory.
⭐⭐⭐Exercise
Build a memory timeline graph using `Canvas`. Record snapshots every 5 seconds and render the last 120 samples as a filled-area chart. Color the fill by zone (used, cached, free).
What You've Learned
  • Use MemAvailable, not MemFree, for accurate usable memory
  • Store values in KB, convert for display only
  • 5-second update interval is sufficient for RAM
  • SmoothedAnimation makes progress bars look fluid
  • Kernel provides MemAvailable since Linux 3.14

Built with VitePress. Licensed under MIT.