Below is a “from zero to usable bar” QuickShell guide that stitches together:
- Tony’s walkthrough (your transcript)
- Tony’s written tutorial version (tonybtw.com)
- Official docs (install + API/type docs) (quickshell.org)
- DeepWiki’s repo-level “getting started” notes (paths, pragmas, runtime dirs) (DeepWiki)
QuickShell (often written “Quickshell”) is a Qt/QML-based toolkit for building Wayland desktop UI like bars, widgets, overlays, lock screens, etc. Tony frames it as a practical Waybar replacement for Hyprland, with Sway support (API differs a bit). (tonybtw.com)
The core mental model:
- You write QML (declarative UI). (quickshell.outfoxxed.me)
- Your UI “binds” to live data (Hyprland IPC objects, timers, process output).
- Most widgets are: run something → parse output → update properties → render Text/Rectangles. Tony calls this pattern out explicitly.
Tony just adds quickshell to system packages. (tonybtw.com)
Tony’s tutorial uses the AUR quickshell-git package: (tonybtw.com) (AUR)
yay -S quickshell-gitThe official install/setup guide also covers release vs master tracking and editor setup. (quickshell.org)
DeepWiki documents build requirements and feature flags (Wayland/Hyprland integrations are feature-gated). (DeepWiki) If you’re on Arch and you update Qt, you may need to rebuild the AUR package (a common “built against old Qt” footgun). (GitHub)
The official install/setup page explicitly recommends a QML grammar + QML LSP (qmlls) setup. (quickshell.org)
This makes QML feel less like wizard runes and more like a normal language (autocomplete, jump-to-definition, type info).
QuickShell’s typical entrypoint is shell.qml under:
~/.config/quickshell/shell.qml(DeepWiki)
If you run qs with no config, Tony hits the expected error: “cannot find default config or shell.qml in any valid config path.”
DeepWiki also notes that the config path is hashed to generate a unique shell identifier and that Quickshell builds a runtime directory tree under XDG_RUNTIME_DIR (instance management, IPC, etc.). (DeepWiki)
Basic:
qsRunning a specific file (handy for iterating through examples):
qs -p ~/.config/testshell/01-hello.qmlThat -p pattern is exactly how Tony’s written tutorial suggests stepping through examples. (tonybtw.com)
Create the config directory + file:
mkdir -p ~/.config/quickshell
$EDITOR ~/.config/quickshell/shell.qmlMinimal QML:
import Quickshell
import QtQuick
FloatingWindow {
visible: true
width: 200
height: 100
Text {
anchors.centerIn: parent
text: "Hello, Quickshell!"
font.pixelSize: 18
}
}This mirrors the tutorial’s baseline: Quickshell + QtQuick, with a floating window that doesn’t reserve screen space. (tonybtw.com)
Run it:
qsTo dock to the top edge (and reserve space), switch to PanelWindow and import Wayland types:
import Quickshell
import Quickshell.Wayland
import QtQuick
PanelWindow {
anchors.top: true
anchors.left: true
anchors.right: true
implicitHeight: 30
color: "#1a1b26"
Text {
anchors.centerIn: parent
text: "My First Bar"
color: "#a9b1d6"
font.pixelSize: 14
}
}Key idea: PanelWindow docks and reserves space, unlike FloatingWindow. (tonybtw.com)
This is the “ok, now it replaces Waybar” moment.
You’ll add:
Quickshell.Hyprlandfor IPC integration (quickshell.outfoxxed.me)QtQuick.LayoutsforRowLayout(tonybtw.com)
Example:
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import QtQuick
import QtQuick.Layouts
PanelWindow {
anchors.top: true
anchors.left: true
anchors.right: true
implicitHeight: 30
color: "#1a1b26"
RowLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 10
Repeater {
model: 9
Text {
// workspace IDs 1..9
property int wsId: index + 1
// live workspace object (null if empty/nonexistent)
property var ws: Hyprland.workspaces.values.find(w => w.id === wsId)
// focused workspace
property bool isActive: Hyprland.focusedWorkspace?.id === wsId
text: wsId
font.pixelSize: 14
font.bold: true
// color logic:
// - cyan if active
// - blue if it exists (has windows)
// - muted if empty
color: isActive ? "#0db9d7" : (ws ? "#7aa2f7" : "#444b6a")
MouseArea {
anchors.fill: parent
onClicked: Hyprland.dispatch("workspace " + wsId)
}
}
}
// spacer pushes workspace block left
Item { Layout.fillWidth: true }
}
}Notes that matter in real configs:
Hyprland.dispatch(...)executes Hyprland dispatchers. (quickshell.outfoxxed.me)- Hyprland workspaces include named workspaces with negative IDs (they sort before numbered ones), so if you get fancy later, account for that. (quickshell.outfoxxed.me)
- Tony uses this same “Repeater + dispatch + color by focused/existing” approach. (tonybtw.com)
This is the standard QuickShell widget pipeline:
Processruns a command (quickshell.outfoxxed.me)SplitParserreads stdout in chunks/lines (quickshell.outfoxxed.me)- A
Timerre-runs it periodically (tonybtw.com) - Properties hold state (so UI can bind)
At the top of your PanelWindow:
PanelWindow {
id: root
// theme
property color colBg: "#1a1b26"
property color colFg: "#a9b1d6"
property color colMuted: "#444b6a"
property color colCyan: "#0db9d7"
property color colBlue: "#7aa2f7"
property color colYellow: "#e0af68"
property string fontFamily: "JetBrainsMono Nerd Font"
property int fontSize: 14
// system state
property int cpuUsage: 0
property int memUsage: 0
property var lastCpuIdle: 0
property var lastCpuTotal: 0
anchors.top: true
anchors.left: true
anchors.right: true
implicitHeight: 30
color: colBg
// ...
}This “define theme once on root, reuse everywhere” approach is straight from the tutorial. (tonybtw.com)
Tony’s example reads the first line of /proc/stat and computes CPU usage from deltas between samples. (tonybtw.com)
import Quickshell.Io
Process {
id: cpuProc
command: ["sh", "-c", "head -1 /proc/stat"]
stdout: SplitParser {
onRead: data => {
if (!data) return
var p = data.trim().split(/\s+/)
// idle = idle + iowait
var idle = parseInt(p[4]) + parseInt(p[5])
// total = user..softirq (slice(1,8))
var total = p.slice(1, 8).reduce((a, b) => a + parseInt(b), 0)
if (lastCpuTotal > 0) {
cpuUsage = Math.round(100 * (1 - (idle - lastCpuIdle) / (total - lastCpuTotal)))
}
lastCpuTotal = total
lastCpuIdle = idle
}
}
Component.onCompleted: running = true
}Tony’s memory widget runs free | grep Mem and turns used/total into a percent. (tonybtw.com)
Process {
id: memProc
command: ["sh", "-c", "free | grep Mem"]
stdout: SplitParser {
onRead: data => {
if (!data) return
var parts = data.trim().split(/\s+/)
var total = parseInt(parts[1]) || 1
var used = parseInt(parts[2]) || 0
memUsage = Math.round(100 * used / total)
}
}
Component.onCompleted: running = true
}Timer {
interval: 2000
running: true
repeat: true
onTriggered: {
cpuProc.running = true
memProc.running = true
}
}This is exactly the “poll every N ms, rerun processes” pattern described in the tutorial. (tonybtw.com)
Tony uses Qt.formatDateTime and updates with a 1-second timer. (tonybtw.com)
Text {
id: clock
text: Qt.formatDateTime(new Date(), "ddd, MMM dd - HH:mm")
color: root.colFg
font.family: root.fontFamily
font.pixelSize: root.fontSize
Timer {
interval: 1000
running: true
repeat: true
onTriggered: clock.text = Qt.formatDateTime(new Date(), "ddd, MMM dd - HH:mm")
}
}This is intentionally “single file, readable, and easy to extend”:
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import Quickshell.Io
import QtQuick
import QtQuick.Layouts
PanelWindow {
id: root
// theme
property color colBg: "#1a1b26"
property color colFg: "#a9b1d6"
property color colMuted: "#444b6a"
property color colCyan: "#0db9d7"
property color colBlue: "#7aa2f7"
property color colYellow: "#e0af68"
property string fontFamily: "JetBrainsMono Nerd Font"
property int fontSize: 14
// state
property int cpuUsage: 0
property int memUsage: 0
property var lastCpuIdle: 0
property var lastCpuTotal: 0
anchors.top: true
anchors.left: true
anchors.right: true
implicitHeight: 30
color: colBg
// CPU
Process {
id: cpuProc
command: ["sh", "-c", "head -1 /proc/stat"]
stdout: SplitParser {
onRead: data => {
if (!data) return
var p = data.trim().split(/\s+/)
var idle = parseInt(p[4]) + parseInt(p[5])
var total = p.slice(1, 8).reduce((a, b) => a + parseInt(b), 0)
if (lastCpuTotal > 0) {
cpuUsage = Math.round(100 * (1 - (idle - lastCpuIdle) / (total - lastCpuTotal)))
}
lastCpuTotal = total
lastCpuIdle = idle
}
}
Component.onCompleted: running = true
}
// MEM
Process {
id: memProc
command: ["sh", "-c", "free | grep Mem"]
stdout: SplitParser {
onRead: data => {
if (!data) return
var parts = data.trim().split(/\s+/)
var total = parseInt(parts[1]) || 1
var used = parseInt(parts[2]) || 0
memUsage = Math.round(100 * used / total)
}
}
Component.onCompleted: running = true
}
// refresh both
Timer {
interval: 2000
running: true
repeat: true
onTriggered: {
cpuProc.running = true
memProc.running = true
}
}
RowLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 10
// Workspaces (1..9)
Repeater {
model: 9
Text {
property int wsId: index + 1
property var ws: Hyprland.workspaces.values.find(w => w.id === wsId)
property bool isActive: Hyprland.focusedWorkspace?.id === wsId
text: wsId
color: isActive ? root.colCyan : (ws ? root.colBlue : root.colMuted)
font.family: root.fontFamily
font.pixelSize: root.fontSize
font.bold: true
MouseArea {
anchors.fill: parent
onClicked: Hyprland.dispatch("workspace " + wsId)
}
}
}
Item { Layout.fillWidth: true }
// Clock
Text {
id: clock
text: Qt.formatDateTime(new Date(), "HH:mm:ss")
color: root.colFg
font.family: root.fontFamily
font.pixelSize: root.fontSize
Timer {
interval: 1000
running: true
repeat: true
onTriggered: clock.text = Qt.formatDateTime(new Date(), "HH:mm:ss")
}
}
Rectangle { width: 1; height: 16; color: root.colMuted }
// Mem + CPU readouts
Text {
text: "MEM " + root.memUsage + "%"
color: root.colFg
font.family: root.fontFamily
font.pixelSize: root.fontSize
}
Rectangle { width: 1; height: 16; color: root.colMuted }
Text {
text: "CPU " + root.cpuUsage + "%"
color: root.colYellow
font.family: root.fontFamily
font.pixelSize: root.fontSize
font.bold: true
}
}
}This is essentially Tony’s end-to-end bar: workspaces, CPU, memory, clock, with the same building blocks he describes. (tonybtw.com)
In your Hyprland config, use exec-once to start it at compositor launch. (Hyprland Wiki)
Example:
exec-once = qs
Tony describes swapping Waybar out and having Hyprland start Quickshell instead.
Hyprland.workspaces,Hyprland.focusedWorkspace, andHyprland.dispatch(...)are the bread and butter. (quickshell.outfoxxed.me)- Workspaces have additional properties and helper functions (like
activate()onHyprlandWorkspace) when you want to go beyond simple dispatch strings. (quickshell.outfoxxed.me)
Process and SplitParser are official types; when in doubt, check the type docs for signals/properties (stderr handling, working directory, lifecycle). (quickshell.outfoxxed.me)
DeepWiki lists supported //@ pragma ... directives like:
Env VAR=VALUE(good forQT_SCALE_FACTOR)IconTheme name- overriding
DataDir,StateDir,CacheDir, etc. (DeepWiki)
These are great for making your shell portable across machines without duct-taping environment variables in launch scripts.
- Run
qsfrom a terminal so you can see errors and logs while you tweak QML. - If
qssays it can’t findshell.qml, verify~/.config/quickshell/shell.qmlexists. (DeepWiki) - On Arch: after a Qt upgrade, rebuild
quickshell-gitif you see warnings about being built against an older Qt version. (GitHub)
- The official docs site includes a Hyprland integration section and type docs that make it easier to build “real UI” (window lists, monitors, toplevel tracking, etc.). (quickshell.outfoxxed.me)
- DeepWiki’s sections on configuration structure and the window system are the bridge from “bar” to “desktop shell.” (DeepWiki)
- Tony explicitly mentions wallpaper managers, dashboards, and lock widgets as natural next projects.
If you want a practical next milestone that doesn’t explode scope: add a layout indicator and a focused window title (both are very “bar-like”, and they force you to learn the Hyprland object model without diving straight into DBus services).