tonybtw.com

tonybtw.com

https://git.tonybtw.com/tonybtw.com.git git://git.tonybtw.com/tonybtw.com.git

Added quickshell

Commit
86bf17f808fd802bd8c714cb97fa9d4d9ff9fccc
Parent
49ec961
Author
tonybtw <tonybtw@tonybtw.com>
Date
2025-12-04 05:14:24

Diff

diff --git a/content/org-files/quickshell.org b/content/org-files/quickshell.org
new file mode 100644
index 0000000..7b6a688
--- /dev/null
+++ b/content/org-files/quickshell.org
@@ -0,0 +1,438 @@
+#+TITLE: Quickshell Tutorial - Build Your Own Bar
+#+AUTHOR: Tony, btw
+#+date: 2025-12-03
+#+HUGO_TITLE: Quickshell Tutorial - Build Your Own Bar
+#+HUGO_FRONT_MATTER_FORMAT: yaml
+#+HUGO_CUSTOM_FRONT_MATTER: :image "/img/quickshell.png" :showTableOfContents true
+#+HUGO_BASE_DIR: ~/repos/tonybtw.com
+#+HUGO_SECTION: tutorial/quickshell
+#+EXPORT_FILE_NAME: index
+#+OPTIONS: toc:nil broken-links:mark
+#+HUGO_AUTO_SET_HEADLINE_SECTION: nil
+#+DESCRIPTION: This is a quick and painless tutorial on how to build a custom status bar using Quickshell, a powerful Qt/QML based shell framework for Wayland.
+
+* 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.
+
+[[https://quickshell.org/docs/v0.2.1/types/][Quickshell Documentation]]
+
+*** NixOS
+#+begin_src nix
+{
+  environment.systemPackages = with pkgs; [
+    quickshell
+  ];
+}
+#+end_src
+
+*** Arch Linux
+#+begin_src sh
+yay -S quickshell-git
+#+end_src
+
+*** Gentoo
+For Gentoo, you'll need to compile from source:
+
+#+begin_src sh
+git clone https://github.com/outfoxxed/quickshell
+cd quickshell
+# Follow the build instructions in their README
+#+end_src
+
+* Running Examples
+
+You can run any of these examples with:
+
+#+begin_src sh
+qs -p ~/.config/testshell/01-hello.qml
+#+end_src
+
+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.
+
+#+begin_src qml
+import Quickshell
+import QtQuick
+
+FloatingWindow {
+    visible: true
+    width: 200
+    height: 100
+
+    Text {
+        anchors.centerIn: parent
+        text: "Hello, Quickshell!"
+        color: "#0db9d7"
+        font.pixelSize: 18
+    }
+}
+#+end_src
+
+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.
+
+#+begin_src qml
+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
+    }
+}
+#+end_src
+
+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.
+
+#+begin_src qml
+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 }
+    }
+}
+#+end_src
+
+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.
+
+#+begin_src qml
+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
+}
+#+end_src
+
+** Running Shell Commands
+
+This is how you run external commands and capture their output. We import =Quickshell.Io= which gives us the =Process= type.
+
+#+begin_src qml
+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
+}
+#+end_src
+
+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.
+
+#+begin_src qml
+Timer {
+    interval: 2000        // Every 2 seconds
+    running: true         // Start immediately
+    repeat: true          // Keep going forever
+    onTriggered: cpuProc.running = true
+}
+#+end_src
+
+* 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
+
+#+begin_src qml
+// 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
+    }
+}
+#+end_src
+
+** Clock
+
+The clock is just a =Text= element with its own timer. Every second it updates the text with the current time.
+
+#+begin_src qml
+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")
+    }
+}
+#+end_src
+
+** Adding Dividers
+
+To visually separate widgets, you can use simple =Rectangle= elements:
+
+#+begin_src qml
+Rectangle { width: 1; height: 16; color: root.colMuted }
+#+end_src
+
+* Complete Bar Example
+
+Here's the final bar with workspaces, CPU, memory, and a clock all together:
+
+#+begin_src qml
+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")
+            }
+        }
+    }
+}
+#+end_src
+
+* 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: [[https://youtube.com/@tony-btw][YouTube]], or my website here: [[https://www.tonybtw.com][tony,btw]]
+
+You can support me here: [[https://ko-fi.com/tonybtw][kofi]]
diff --git a/content/tutorial/quickshell/index.md b/content/tutorial/quickshell/index.md
new file mode 100644
index 0000000..92198f6
--- /dev/null
+++ b/content/tutorial/quickshell/index.md
@@ -0,0 +1,458 @@
+---
+title: "Quickshell Tutorial - Build Your Own Bar"
+author: ["Tony", "btw"]
+description: "This is a quick and painless tutorial on how to build a custom status bar using Quickshell, a powerful Qt/QML based shell framework for Wayland."
+date: 2025-12-03
+draft: false
+image: "/img/quickshell.png"
+showTableOfContents: true
+---
+
+## Intro {#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 {#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](https://quickshell.org/docs/v0.2.1/types/)
+
+
+#### NixOS {#nixos}
+
+```nix
+{
+  environment.systemPackages = with pkgs; [
+    quickshell
+  ];
+}
+```
+
+
+#### Arch Linux {#arch-linux}
+
+```sh
+yay -S quickshell-git
+```
+
+
+#### Gentoo {#gentoo}
+
+For Gentoo, you'll need to compile from source:
+
+```sh
+git clone https://github.com/outfoxxed/quickshell
+cd quickshell
+# Follow the build instructions in their README
+```
+
+
+## Running Examples {#running-examples}
+
+You can run any of these examples with:
+
+```sh
+qs -p ~/.config/testshell/01-hello.qml
+```
+
+Just swap out the filename as we go through each one.
+
+
+## 01 - Hello World {#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.
+
+```qml
+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 {#02-empty-bar}
+
+Alright, let's turn this into an actual bar that docks to the top of your screen.
+
+```qml
+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 {#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.
+
+```qml
+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 {#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 {#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.
+
+```qml
+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 {#running-shell-commands}
+
+This is how you run external commands and capture their output. We import `Quickshell.Io` which gives us the `Process` type.
+
+```qml
+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 {#timers}
+
+To update the CPU usage periodically, we use a `Timer`. This one fires every 2 seconds and re-runs the CPU process.
+
+```qml
+Timer {
+    interval: 2000        // Every 2 seconds
+    running: true         // Start immediately
+    repeat: true          // Keep going forever
+    onTriggered: cpuProc.running = true
+}
+```
+
+
+## 05 - Adding Widgets {#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 {#memory-widget}
+
+```qml
+// 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 {#clock}
+
+The clock is just a `Text` element with its own timer. Every second it updates the text with the current time.
+
+```qml
+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 {#adding-dividers}
+
+To visually separate widgets, you can use simple `Rectangle` elements:
+
+```qml
+Rectangle { width: 1; height: 16; color: root.colMuted }
+```
+
+
+## Complete Bar Example {#complete-bar-example}
+
+Here's the final bar with workspaces, CPU, memory, and a clock all together:
+
+```qml
+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 {#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 {#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 {#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](https://youtube.com/@tony-btw), or my website here: [tony,btw](https://www.tonybtw.com)
+
+You can support me here: [kofi](https://ko-fi.com/tonybtw)
diff --git a/static/img/quickshell.png b/static/img/quickshell.png
new file mode 100644
index 0000000..b16a7f9
Binary files /dev/null and b/static/img/quickshell.png differ