Quickshell Tutorial - Build Your Own Bar

Quickshell Tutorial - Build Your Own Bar

Intro

What’s up guys, my name is Tony, and today I’m gonna give you a quick and painless introduction to Quickshell.

Quickshell is a full shell framework built on Qt/QML. You can build pretty much any desktop widget you can imagine with it - bars, dashboards, wallpaper managers, screen lock widgets, and more.

We’re going to do a basic overview of how to create a bar using Quickshell today. In future tutorials I’ll cover creating a wallpaper manager, building a dashboard overlay, and making a screen lock widget. But for now, let’s start with the fundamentals by building a functional status bar step by step.

Install Quickshell

Alright so I’m on NixOS today, but this is going to work on Arch, Gentoo, and other distributions. I’ll leave install instructions for all of those below.

Quickshell Documentation

NixOS

{
  environment.systemPackages = with pkgs; [
    quickshell
  ];
}

Arch Linux

yay -S quickshell-git

Gentoo

For Gentoo, you’ll need to compile from source:

git clone https://github.com/outfoxxed/quickshell
cd quickshell
# Follow the build instructions in their README

Running Examples

You can run any of these examples with:

qs -p ~/.config/testshell/01-hello.qml

Just swap out the filename as we go through each one.

01 - Hello World

Quickshell has excellent documentation, and we’re going to be following that today. Let’s start with the absolute basics - just getting something on screen.

import Quickshell
import QtQuick

FloatingWindow {
    visible: true
    width: 200
    height: 100

    Text {
        anchors.centerIn: parent
        text: "Hello, Quickshell!"
        color: "#0db9d7"
        font.pixelSize: 18
    }
}

So what’s going on here?

Every Quickshell config starts with imports. We’re pulling in Quickshell for the core stuff and QtQuick for basic UI elements like Text and Rectangle.

We’re using FloatingWindow as our root element. This is just a regular floating window - it doesn’t dock to any edges or reserve any screen space. We’re setting it to 200x100 pixels and making it visible.

The Text element is pretty self-explanatory. The anchors.centerIn: parent bit is QML’s layout system - it just centers the text inside its parent container.

02 - Empty Bar

Alright, let’s turn this into an actual bar that docks to the top of your screen.

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
    }
}

The big change here is we’re using PanelWindow as our root element instead of FloatingWindow. This is a Wayland-specific thing (hence the new import), and it lets us dock the window to screen edges.

Setting anchors.top, anchors.left, and anchors.right to true tells it to stick to the top edge and span the full width. The implicitHeight: 30 gives us a 30 pixel tall bar.

Unlike a floating window, a PanelWindow actually reserves space - your other windows won’t overlap with it.

03 - Workspaces

So we are on Hyprland today, lets add quickshell to our Hyprland config.

Now let’s add some actual functionality - workspace indicators that show which workspace you’re on and let you click to switch.

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

        Repeater {
            model: 9

            Text {
                property var ws: Hyprland.workspaces.values.find(w => w.id === index + 1)
                property bool isActive: Hyprland.focusedWorkspace?.id === (index + 1)
                text: index + 1
                color: isActive ? "#0db9d7" : (ws ? "#7aa2f7" : "#444b6a")
                font { pixelSize: 14; bold: true }

                MouseArea {
                    anchors.fill: parent
                    onClicked: Hyprland.dispatch("workspace " + (index + 1))
                }
            }
        }

        Item { Layout.fillWidth: true }
    }
}

Okay, there’s a lot more going on here. Let me break it down.

We’re importing Quickshell.Hyprland which gives us access to Hyprland’s IPC.

QtQuick.Layouts gives us RowLayout, which arranges its children horizontally. Way easier than manually positioning everything.

The Repeater is super useful - it takes a model (in this case, just the number 9) and creates that many copies of whatever’s inside it. Each copy gets an index variable (0-8).

For each workspace number, we’re looking up the actual workspace from the window manager with Hyprland.workspaces.values.find(). This gives us live data - when workspaces change, the bar updates automatically. We also check if it’s the active workspace using Hyprland.focusedWorkspace.

The color logic is straightforward: cyan if it’s the active workspace, blue if it exists but isn’t active, and muted gray if there’s no windows on that workspace.

The MouseArea makes the whole thing clickable, and Hyprland.dispatch() sends commands to Hyprland. So clicking on “3” runs workspace 3.

That Item { Layout.fillWidth: true } at the end is just a spacer - it pushes everything to the left.

04 - System Stats

Now let’s add some system stats. This is where things get interesting because we need to run shell commands and parse their output.

Theme Properties

Instead of hardcoding colors everywhere, we define them once on the PanelWindow. Now you can reference root.colBg anywhere in your config, and if you want to change your color scheme, you only have to do it in one place.

PanelWindow {
    id: root
    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
}

Running Shell Commands

This is how you run external commands and capture their output. We import Quickshell.Io which gives us the Process type.

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
}

The command is an array - first element is the program, rest are arguments. The SplitParser attached to stdout calls onRead for each line of output. Setting running = true triggers the process to run.

Timers

To update the CPU usage periodically, we use a Timer. This one fires every 2 seconds and re-runs the CPU process.

Timer {
    interval: 2000        // Every 2 seconds
    running: true         // Start immediately
    repeat: true          // Keep going forever
    onTriggered: cpuProc.running = true
}

05 - Adding Widgets

Let’s expand our bar with a clock and memory usage. This builds on the same patterns - more Process calls and Timer elements.

Memory Widget

// Add to your system data properties
property int memUsage: 0

// Memory process
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
}

// Update your timer to run both processes
Timer {
    interval: 2000
    running: true
    repeat: true
    onTriggered: {
        cpuProc.running = true
        memProc.running = true
    }
}

Clock

The clock is just a Text element with its own timer. Every second it updates the text with the current time.

Text {
    id: clock
    text: Qt.formatDateTime(new Date(), "ddd, MMM dd - HH:mm")

    Timer {
        interval: 1000
        running: true
        repeat: true
        onTriggered: clock.text = Qt.formatDateTime(new Date(), "ddd, MMM dd - HH:mm")
    }
}

Adding Dividers

To visually separate widgets, you can use simple Rectangle elements:

Rectangle { width: 1; height: 16; color: root.colMuted }

Complete Bar Example

Here’s the final bar with workspaces, CPU, memory, and a clock all together:

import Quickshell
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Hyprland
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

    // System data
    property int cpuUsage: 0
    property int memUsage: 0
    property var lastCpuIdle: 0
    property var lastCpuTotal: 0

    // Processes and timers here...

    anchors.top: true
    anchors.left: true
    anchors.right: true
    implicitHeight: 30
    color: root.colBg

    RowLayout {
        anchors.fill: parent
        anchors.margins: 8
        spacing: 8

        // Workspaces
        Repeater {
            model: 9
            Text {
                property var ws: Hyprland.workspaces.values.find(w => w.id === index + 1)
                property bool isActive: Hyprland.focusedWorkspace?.id === (index + 1)
                text: index + 1
                color: isActive ? root.colCyan : (ws ? root.colBlue : root.colMuted)
                font { family: root.fontFamily; pixelSize: root.fontSize; bold: true }
                MouseArea {
                    anchors.fill: parent
                    onClicked: Hyprland.dispatch("workspace " + (index + 1))
                }
            }
        }

        Item { Layout.fillWidth: true }

        // CPU
        Text {
            text: "CPU: " + cpuUsage + "%"
            color: root.colYellow
            font { family: root.fontFamily; pixelSize: root.fontSize; bold: true }
        }

        Rectangle { width: 1; height: 16; color: root.colMuted }

        // Memory
        Text {
            text: "Mem: " + memUsage + "%"
            color: root.colCyan
            font { family: root.fontFamily; pixelSize: root.fontSize; bold: true }
        }

        Rectangle { width: 1; height: 16; color: root.colMuted }

        // Clock
        Text {
            id: clock
            color: root.colBlue
            font { family: root.fontFamily; pixelSize: root.fontSize; bold: true }
            text: Qt.formatDateTime(new Date(), "ddd, MMM dd - HH:mm")
            Timer {
                interval: 1000
                running: true
                repeat: true
                onTriggered: clock.text = Qt.formatDateTime(new Date(), "ddd, MMM dd - HH:mm")
            }
        }
    }
}

Key Concepts Summary

Concept Description
FloatingWindow A regular floating window, doesn’t dock
PanelWindow Docks to screen edges, reserves space
RowLayout Arranges children horizontally
Repeater Creates multiple copies of a component
Process Runs shell commands and captures output
Timer Triggers actions at intervals
MouseArea Makes elements clickable
anchors QML’s layout system for positioning
property Declare custom variables on components

Next Steps

That’s the core of it. Quickshell can do way more than just bars though - you can build:

  • Wallpaper managers with smooth transitions
  • Dashboard overlays with system stats
  • Screen lock widgets
  • Notification centers
  • Application launchers

Check out the Quickshell documentation for more advanced features and the full API reference.

Final Thoughts

Thanks so much for checking out this tutorial. If you got value from it, and you want to find more tutorials like this, check out my youtube channel here: YouTube, or my website here: tony,btw

You can support me here: kofi