Processes & Shell Commands
The Problem
A desktop shell needs to interact with the system: check CPU usage, get the list of connected monitors, read network statistics, or launch applications. These tasks require running external processes. You cannot do them from pure QML.
The Naive Approach
QML does not have a built-in way to run shell commands. Some developers attempt to use JavaScript's XMLHttpRequest with file:// URLs or try to embed a C++ plugin. Neither works well for simple system calls.
// This does NOT run shell commands:
function getCpuUsage() {
// XMLHttpRequest cannot execute binaries
return "N/A"
}A process is like a phone call. You dial a number (the command), speak (stdin), listen (stdout), and hang up when done. Quickshell's Process type manages this call for you — it handles the dialing, captures the response, and tells you when the call ends.
The Idea
Quickshell provides the Process type from the Quickshell import. It runs a child process, captures stdout/stderr, and emits signals when data arrives or the process exits. You can run one-shot commands (run("cmd")) or spawn long-lived processes with streaming output.
Let's Build It
import Quickshell
import QtQuick
import QtQuick.Layouts
PanelWindow {
height: 36
anchors { top: true; left: true; right: true }
exclusiveZone: height
RowLayout {
anchors { fill: parent; leftMargin: 8; rightMargin: 8 }
Text {
id: cpuText
text: "CPU: --"
color: "#c0caf5"
font.pixelSize: 13
}
Item { Layout.fillWidth: true }
Text {
id: memText
text: "MEM: --"
color: "#a9b1d6"
font.pixelSize: 13
}
}
Process {
id: cpuProcess
command: ["sh", "-c", "top -bn1 | grep 'Cpu(s)' | awk '{print $2}'"]
onStdout: (data) => {
cpuText.text = "CPU: " + data.trim() + "%"
}
onFinished: (exitCode) => {
if (exitCode === 0 && running) running = true
}
}
Process {
id: memProcess
command: ["sh", "-c", "free -m | awk '/Mem:/ {print $3 \"M / \" $2 \"M\"}'"]
onStdout: (data) => {
memText.text = "MEM: " + data.trim()
}
}
Timer {
interval: 5000
running: true
repeat: true
onTriggered: {
cpuProcess.running = true
memProcess.running = true
}
}
}Let's Improve It
Running a shell pipeline via ["sh", "-c", "..."] every 5 seconds is wasteful. For frequently read values, use a persistent process with streaming output:
Process {
id: topProcess
command: ["sh", "-c", "top -bn1 -w512 | grep --line-buffered 'Cpu(s)'"]
onStdout: (data) => {
var match = data.match(/(\d+\.?\d*)\s*id/)
if (match) {
var idle = parseFloat(match[1])
cpuUsage = Math.round(100 - idle)
}
}
}For reliable parsing, pipe through a dedicated formatter:
Process {
command: ["sh", "-c", "cat /proc/stat | head -1"]
onStdout: (data) => {
var parts = data.trim().split(/\s+/)
total = parts.slice(1).reduce((a, b) => a + parseInt(b), 0)
idle = parseInt(parts[4])
}
}Forgetting to set readableChannel. By default, Process captures all channels. If your command produces binary output on stderr, set readableChannel: ProcessChannel.Stdout to avoid buffering noise.
Not shell-escaping arguments. Do not build command strings with string concatenation. Always pass arguments as separate array elements: command: ["sh", "-c", "echo $HOME"] instead of command: "echo $HOME".
Letting processes accumulate. A Process with running: true that never exits will leak system resources. Always set a timeout or ensure the process terminates.
Under the Hood
Process wraps QProcess from Qt Core. When you set running: true, Quickshell calls QProcess::start() with the provided command and arguments. Output is read asynchronously via readyReadStandardOutput and emitted through the onStdout signal. The process runs in a separate OS process — if your shell crashes, child processes may become orphaned unless they have prctl(PR_SET_PDEATHSIG) or equivalent.
The command property accepts a list of strings. Quickshell converts this to QProcess::setProgram() and QProcess::setArguments(). For shell pipelines, you must explicitly invoke sh -c "pipeline" because QProcess does not parse shell syntax — it calls execvp() directly.
Exercises
- Process runs external commands asynchronously
- Use onStdout to capture output line-by-line
- onFinished fires when the process exits with the exit code
- Always pass command arguments as a string list, not concatenated strings
- Use sh -c for shell pipelines
- Set running: true to start, false to stop