agentsclimarketplace

Quickshell

Skill hicham-bouchikhi/quickshell-skills/skills/quickshell

QuickShell skill for Claude Code - QML desktop shell framework reference

Install
npx -y skills add hicham-bouchikhi/quickshell-skills --skill quickshell

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Build, debug, and extend QuickShell configs — QML-based desktop shell/widget framework for Linux Wayland compositors. USE FOR: writing shell.qml configs, bars, panels, popups, widgets, process runners, IPC, Hyprland/Wayland integration, notifications, system tray, media controls (MPRIS), audio (PipeWire), power (UPower), multi-monitor layouts, singletons, Scope modules, IpcHandler, PersistentProperties, Connections, inline components, typed QML functions. DO NOT USE FOR: generic QML/Qt app development unrelated to desktop shells, non-Linux platforms.

SKILL.md

25.2 KB, as published. Nobody here has run it

QuickShell

QuickShell is a QML-based framework for building custom desktop shells — bars, panels, popups, widgets — on Linux Wayland compositors. Configs are pure QML files; no build system required.

When to Use

  • User wants to create or modify a QuickShell shell.qml config
  • User needs help with services, Hyprland, Wayland, MPRIS, notifications, system tray, PipeWire, or UPower
  • User wants to spawn processes, parse output, do IPC, or persist state across reloads
  • User asks about any type from the Quickshell.* module namespace

When Not to Use

  • User is building a standalone Qt application (not a shell/bar/widget)
  • User is on X11 only with no Wayland layer shell support
  • User is asking about a different shell framework (AGS, Waybar, eww, etc.)

Config Setup

ItemDetail
Entry point~/.config/quickshell/shell.qml (or named subfolder: ~/.config/quickshell/<name>/shell.qml)
Run default configquickshell
Run named configquickshell -c <name>
Run arbitrary filequickshell -p /path/to/shell.qml
Reload running instancequickshell --reload
LSP supportCreate empty .qmlls.ini next to shell.qml; QuickShell auto-fills it
Set env vars in config//@ pragma Env VAR=value at the top of shell.qml
Enable platform menus//@ pragma UseQApplication at the top of shell.qmlrequired for QsMenuAnchor.open() (system tray context menus, right-click menus). Restart QuickShell after adding; --reload is not enough.

Inputs

InputRequiredDescription
Goal or existing QML snippetYesWhat the user wants to build or fix
CompositorNoHyprland, generic Wayland, etc. — determines which modules to import
Target monitorNoSingle vs. multi-monitor affects whether to use Variants

Core Concepts

shell.qml entry point

//@ pragma Env QSG_RENDER_LOOP=threaded

import "modules"
import "services"
import Quickshell

ShellRoot {
    Background {}
    Bar {}
    Notifications {}
    Shortcuts {}
    BatteryMonitor {}
}

ShellRoot is the root element. Child elements are typically Scope-based modules or window types.

PanelWindow — anchored bar/panel

import Quickshell
import QtQuick

PanelWindow {
    anchors { top: true; left: true; right: true }
    implicitHeight: 30

    Text {
        anchors.centerIn: parent
        text: "hello world"
        color: "white"
    }
}

Multi-monitor with Variants

Spawn one instance per screen. Instances appear/disappear automatically as monitors connect/disconnect.

import Quickshell

Variants {
    model: Quickshell.screens

    PanelWindow {
        required property ShellScreen modelData
        screen: modelData

        anchors { top: true; left: true; right: true }
        implicitHeight: 30
    }
}

Singleton — global shared service

Add pragma Singleton to a capitalized .qml file. Access it by filename from anywhere in the config. The root element must be Singleton {}, not QtObject or Item.

// services/Audio.qml
pragma Singleton

import Quickshell
import Quickshell.Services.Pipewire

Singleton {
    id: root

    readonly property PwNode sink: Pipewire.defaultAudioSink
    readonly property bool muted: !!sink?.audio?.muted
    readonly property real volume: sink?.audio?.volume ?? 0

    function setVolume(newVolume: real): void {
        if (sink?.ready && sink?.audio)
            sink.audio.volume = Math.max(0, Math.min(1.0, newVolume));
    }
}

Scope — non-singleton module

Use Scope (not Singleton) for modules that don't need global access, or that are instantiated per context (e.g., keyboard shortcuts, battery watchers). Scope participates in the reload graph but is not globally addressable.

// modules/BatteryMonitor.qml
import Quickshell
import Quickshell.Services.UPower
import QtQuick

Scope {
    Connections {
        target: UPower

        function onOnBatteryChanged(): void {
            if (UPower.onBattery)
                console.log("Charger unplugged")
        }
    }

    Timer {
        id: hibernateTimer
        interval: 5000
        onTriggered: Quickshell.execDetached(["systemctl", "hibernate"])
    }
}

pragma ComponentBehavior: Bound

Always add this pragma when a file uses component definitions or signal handlers inside nested components. Without it, inner component blocks cannot reliably access outer id references or required properties.

pragma Singleton
pragma ComponentBehavior: Bound

import Quickshell
import QtQuick

Singleton {
    id: root

    component Notif: QtObject {
        required property string summary
        // Can safely reference `root` here because of ComponentBehavior: Bound
        function close(): void {
            root.list = root.list.filter(n => n !== this);
            destroy();
        }
    }
}

Typed Functions and Properties

QuickShell uses QML 6 — always annotate function parameters and return types:

function setVolume(newVolume: real): void { ... }
function getStreamVolume(stream: PwNode): real { return stream?.audio?.volume ?? 0 }
function getStreamName(stream: PwNode): string { return stream?.name ?? "Unknown" }
function isDndEnabled(): bool { return props.dnd }
function cycleWorkspace(direction: string): void { ... }

Typed property declarations:

readonly property PwNode sink: Pipewire.defaultAudioSink
readonly property list<PwNode> sinks: nodes.sinks
property list<Notif> list: []
readonly property HyprlandMonitor focusedMonitor: Hyprland.focusedMonitor

Null Safety

Use ?. (optional chaining) and ?? (null coalescing) freely — they are idiomatic in QuickShell code:

readonly property bool muted: !!sink?.audio?.muted      // double-! coerces to bool
readonly property real volume: sink?.audio?.volume ?? 0
readonly property string layout: keyboard?.activeKeymap ?? "Unknown"
readonly property int activeWsId: focusedWorkspace?.id ?? 1

Connections

Connections is the primary way to react to signals on external objects. Use the function onXxx() syntax (not the old onXxx: property syntax):

Connections {
    target: Hyprland

    function onRawEvent(event: HyprlandEvent): void {
        if (event.name === "configreloaded")
            root.reload();
        else if (["openwindow", "closewindow"].includes(event.name))
            Hyprland.refreshToplevels();
    }
}

Connections {
    target: UPower.displayDevice

    function onPercentageChanged(): void {
        const p = UPower.displayDevice.percentage * 100;
        if (p <= 10) console.warn("Battery critical:", p);
    }
}

Multiple Connections blocks in one file are fine and common.


Readonly Properties with Complex Expressions

Use JS array methods directly in readonly property bindings:

// Partition Pipewire nodes into sinks, sources, and streams
readonly property var nodes: Pipewire.nodes.values.reduce((acc, node) => {
    if (!node.isStream) {
        if (node.isSink) acc.sinks.push(node);
        else if (node.audio) acc.sources.push(node);
    } else if (node.audio) {
        acc.streams.push(node);
    }
    return acc;
}, { sinks: [], sources: [], streams: [] })

readonly property list<PwNode> sinks: nodes.sinks

// Find active network access point
readonly property AccessPoint active: networks.find(n => n.active) ?? null

// Filter only open special workspaces
readonly property var openSpecials: workspaces.values
    .filter(w => w.name.startsWith("special:") && w.lastIpcObject.windows > 0)

Inline Components

Define reusable local types inside a file using component. Requires pragma ComponentBehavior: Bound.

pragma Singleton
pragma ComponentBehavior: Bound

import Quickshell
import Quickshell.Services.Notifications
import QtQuick

Singleton {
    id: root

    property list<Notif> list: []
    readonly property list<Notif> popups: list.filter(n => n.popup)

    component Notif: QtObject {
        property bool popup
        property bool closed
        property string summary
        property string body
        property int urgency
        property var locks: new Set()

        function close(): void {
            closed = true;
            if (locks.size === 0) {
                root.list = root.list.filter(n => n !== this);
                destroy();
            }
        }
    }

    Component { id: notifComp; Notif {} }

    NotificationServer {
        onNotification: notif => {
            const obj = notifComp.createObject(root, {
                summary: notif.summary,
                body: notif.body,
                popup: true
            });
            root.list = [obj, ...root.list];
        }
    }
}

IpcHandler — CLI control

Expose functions to external tools via qs ipc call <target> <function> [args...]. Return types matter: use string, bool, real, or void.

IpcHandler {
    target: "audio"

    function getVolume(): real { return Audio.volume }
    function setVolume(v: real): void { Audio.setVolume(v) }
    function toggleMute(): void {
        if (Audio.sink?.audio)
            Audio.sink.audio.muted = !Audio.sink.audio.muted;
    }
}

IpcHandler {
    target: "notifs"

    function isDndEnabled(): bool { return props.dnd }
    function toggleDnd(): void { props.dnd = !props.dnd }
    function clear(): void {
        for (const n of root.list.slice()) n.close();
    }
    function listActive(): string {
        return root.notClosed.map(n => n.summary).join("\n");
    }
}

Call from the terminal:

qs ipc call audio setVolume 0.5
qs ipc call notifs toggleDnd
qs ipc call notifs isDndEnabled
qs ipc call hypr listSpecialWorkspaces

PersistentProperties

Survives config reload. Use reloadableId (not reloadSource) to key the storage.

PersistentProperties {
    id: props
    reloadableId: "notifs"

    property bool dnd
    property string lastWorkspace: ""
}

Access like any object: props.dnd, props.lastWorkspace = "special:scratch".


FileView — reading and writing files

FileView {
    path: `${Quickshell.configDir}/state/notifs.json`

    onLoaded: {
        const data = JSON.parse(text());
        for (const item of data)
            root.list.push(notifComp.createObject(root, item));
    }

    onLoadFailed: err => {
        if (err === FileViewError.FileNotFound)
            setText("[]");   // initialize empty file
    }
}

setText() writes back to the file. Use JsonAdapter for typed JSON access.


Process — running commands

import Quickshell.Io

// One-shot: collect full output
Process {
    id: dateProc
    command: ["date", "+%H:%M"]   // always an array, never a plain string
    running: true

    stdout: StdioCollector {
        onStreamFinished: root.timeText = text.trim()
    }
}

// Streaming: line-by-line output
Process {
    command: ["nmcli", "monitor"]
    running: true

    stdout: SplitParser {
        onRead: line => root.handleNmcliLine(line)
    }
}

Periodic refresh:

Timer {
    interval: 60000; repeat: true; running: true
    onTriggered: dateProc.running = true
}

LazyLoader — conditional/deferred components

LazyLoader {
    id: dialogLoader
    active: false   // set true to load, false to unload and destroy

    FloatingWindow {
        // only created when dialogLoader.active = true
    }
}

Utility Functions

// Fire-and-forget (no stdout capture):
Quickshell.execDetached(["systemctl", "hibernate"])
Quickshell.execDetached(["notify-send", "Hello"])

// Read environment variable:
Quickshell.env("XKB_RULES_PATH") || "/usr/share/X11/xkb/rules/base.lst"

// Deferred execution (next event loop tick):
Qt.callLater(() => { root.initialize() })
Qt.callLater(() => { root.sync() }, 100)   // with ms delay

Import Structure

Three tiers — always in this order:

// 1. Local project imports
import "modules"
import qs.services
import qs.config

// 2. Framework/plugin imports (if any)
import Caelestia.Services

// 3. Quickshell + Qt imports
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import Quickshell.Services.Pipewire
import QtQuick
import QtQuick.Layouts

Module Reference

Quickshell (core)

TypePurpose
ShellRootRoot config element; host for all Scope/window children
SingletonBase type for global singleton services (pragma Singleton required)
ScopeNon-singleton module; participates in reload graph
PanelWindowPanel attached to screen edges; reserves screen space
FloatingWindowStandard OS window
PopupWindowTemporary popup; position with PopupAnchor
PopupAnchorAnchor spec used by PopupWindow / QsMenuAnchor; properties: window, item, rect, edges (see Edges), gravity, adjustment
EdgesFlag enum for anchor edges: Edges.Top, Edges.Bottom, Edges.Left, Edges.Right. Combinable with |
QsMenuAnchorDisplays a QsMenuHandle (e.g. SystemTrayItem.menu) as a platform menu. Set menu + anchor.item/anchor.edges, then call open(). Requires //@ pragma UseQApplication
QsMenuHandleOpaque handle returned by services (tray menus, etc.); cannot be shown directly — pass to a QsMenuAnchor
VariantsSpawns one component instance per model entry
LazyLoaderLoad/unload a component on demand via active
BoundComponentComponent loader with initial properties
PersistentPropertiesState that survives --reload; key with reloadableId
SystemClockSystem time with configurable precision
ElapsedTimerMeasure time between events
DesktopEntriesIndex of .desktop application entries
ColorQuantizerExtract palette from an image
ObjectModel / ObjectRepeaterModel/repeater for non-Item objects
ScriptModelQML model from a JS expression

Quickshell.Io

TypePurpose
ProcessSpawn child process; command must be a string array
SplitParserStream stdout split by delimiter; onRead: line => ...
StdioCollectorBuffer all stdout; onStreamFinished fires on exit
FileViewRead/write small files; onLoaded, onLoadFailed, setText()
JsonAdapterTyped JSON access through FileView
Socket / SocketServerUnix domain socket IPC
IpcHandlerExpose typed functions to qs ipc call

Quickshell.Hyprland

TypePurpose
HyprlandMain singleton; toplevels, workspaces, monitors, activeToplevel, focusedWorkspace, focusedMonitor
HyprlandWindowHyprland-specific window properties
HyprlandWorkspaceWorkspace state; id, name, lastIpcObject
HyprlandMonitorMonitor state; lastIpcObject
HyprlandToplevelTop-level surface
HyprlandFocusGrabGrab keyboard/mouse focus
GlobalShortcutRegister a global keybind
HyprlandEventLive IPC event; name, data

Refresh methods: Hyprland.refreshWorkspaces(), Hyprland.refreshMonitors(), Hyprland.refreshToplevels(), Hyprland.dispatch(request).

Quickshell.Wayland

TypePurpose
WlrLayershellWlroots layer-shell window
WlrLayerBackground, Bottom, Top, Overlay
WlrKeyboardFocusKeyboard focus mode for layer surfaces
ToplevelManagerList all open windows via foreign-toplevel
ToplevelA window from another application
WlSessionLock / WlSessionLockSurfaceLockscreen implementation
ScreencopyViewCapture and display a window or monitor

Quickshell.Services.Mpris

TypePurpose
MprisEntry point; Mpris.players
MprisPlayertrackTitle, trackArtist, playbackState, play(), pause(), next(), previous(), position, length
MprisPlaybackStatePlaying, Paused, Stopped
MprisLoopStateNone, Track, Playlist

Quickshell.Services.Notifications

TypePurpose
NotificationServerDaemon; onNotification: notif => { }, keepOnReload, actionsSupported, etc.
Notificationsummary, body, appName, appIcon, urgency, image, actions, dismiss()
NotificationActionidentifier, text, invoke()
NotificationUrgencyLow, Normal, Critical
NotificationCloseReasonWhy a notification closed

Quickshell.Services.SystemTray

TypePurpose
SystemTrayEntry point; SystemTray.items
SystemTrayItemicon, tooltip, title, status, category, hasMenu, onlyMenu, menu (→ QsMenuHandle), activate(), secondaryActivate(), scroll(delta, horizontal)
StatusActive, Passive, NeedsAttention

Opening a tray item's context menu. SystemTrayItem.menu is a QsMenuHandle — calling .open() on it directly does nothing. You must display it via a QsMenuAnchor (from Quickshell). Also requires //@ pragma UseQApplication in shell.qml.

pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.SystemTray
import QtQuick

Row {
    QsMenuAnchor { id: menuAnchor }

    Repeater {
        model: SystemTray.items
        delegate: Rectangle {
            id: item
            required property SystemTrayItem modelData
            width: 28; height: 24

            IconImage { anchors.centerIn: parent; source: item.modelData.icon; implicitSize: 16 }

            MouseArea {
                anchors.fill: parent
                hoverEnabled: true
                acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
                onClicked: mouse => {
                    if (mouse.button === Qt.LeftButton) {
                        if (item.modelData.onlyMenu && item.modelData.hasMenu) {
                            menuAnchor.menu = item.modelData.menu
                            menuAnchor.anchor.item = item
                            menuAnchor.anchor.edges = Edges.Bottom
                            menuAnchor.open()
                        } else {
                            item.modelData.activate()
                        }
                    } else if (mouse.button === Qt.MiddleButton) {
                        item.modelData.secondaryActivate()
                    } else if (mouse.button === Qt.RightButton && item.modelData.hasMenu) {
                        menuAnchor.menu = item.modelData.menu
                        menuAnchor.anchor.item = item
                        menuAnchor.anchor.edges = Edges.Bottom
                        menuAnchor.open()
                    }
                }
            }
        }
    }
}

Key pieces:

  • One shared QsMenuAnchor per widget — reassign menu and anchor on each click.
  • anchor is a PopupAnchor: set anchor.item (the clicked delegate) and anchor.edges (from the Edges enum: Edges.Top, Edges.Bottom, Edges.Left, Edges.Right, combinable with |).
  • Right-click convention opens the menu; left-click usually calls activate(). Check onlyMenu for icons that have no activate action (e.g. some indicator applets).
  • Without //@ pragma UseQApplication you get: Cannot call QsMenuAnchor.open() as quickshell was not started in QApplication mode.

Quickshell.Services.Pipewire

TypePurpose
PipewireEntry point; nodes, links, defaultAudioSink, defaultAudioSource, preferredDefaultAudioSink
PwNodeAudio node; audio (→ PwNodeAudio), isSink, isStream, ready, name, description, applicationName
PwNodeAudiovolume, muted, channels
PwLink / PwLinkGroupConnection between nodes
PwObjectTrackerKeep objects alive: objects: [...sinks, ...sources]
PwNodeLinkTrackerTrack links to/from a node

Quickshell.Services.UPower

TypePurpose
UPowerSingleton; onBattery, displayDevice
UPower.displayDevicepercentage (0–1), state, timeToEmpty, timeToFull

Quickshell.Widgets

TypePurpose
IconImageRenders XDG theme icons by name
ClippingRectangleRectangle that clips children inside its border radius
WrapperRectangle / WrapperItemManages size/position of a single visual child
WrapperMouseAreaMouseArea wrapping a single visual child

Workflow

Step 1: Pick the right root type

SituationRoot type
Global service accessed by name everywhereSingleton {} with pragma Singleton
Module that reacts to events but isn't globalScope {}
Panel/bar attached to screen edgePanelWindow {}
Standard windowFloatingWindow {}
Multi-monitor instance of anythingVariants { model: Quickshell.screens }

Step 2: Map goal to modules

GoalModule(s)
Bar / panelQuickshell (PanelWindow)
Run shell commandsQuickshell.Io (Process, StdioCollector)
Workspaces (Hyprland)Quickshell.Hyprland
Media controlsQuickshell.Services.Mpris
Notification popupQuickshell.Services.Notifications
System trayQuickshell.Services.SystemTray
Volume / audioQuickshell.Services.Pipewire
BatteryQuickshell.Services.UPower
LockscreenQuickshell.Wayland (WlSessionLock)
Window listQuickshell.Wayland (ToplevelManager)
CLI controlQuickshell.Io (IpcHandler)
Persist state across reloadQuickshell (PersistentProperties)

Step 3: Write the QML

  • Add pragma ComponentBehavior: Bound whenever the file uses component definitions
  • Annotate all function parameters and return types
  • Use ?. and ?? for nullable Quickshell model objects
  • Use Connections { target: X; function onY() {} } for all signal reactions
  • Extract shared state to pragma Singleton files (capitalized names, Singleton {} root)
  • Use PersistentProperties with reloadableId for state that must survive --reload
  • Add PwObjectTracker { objects: [...] } when holding Pipewire node references

Step 4: Reload and test

quickshell --reload
quickshell -p shell.qml

Step 5: Debug

  • QML errors print to stderr — always run from a terminal
  • console.log(...) / console.warn(...) for runtime values
  • qs ipc call <target> <fn> to exercise IpcHandler functions live

Validation

  • pragma ComponentBehavior: Bound present in files that define component types
  • All function parameters and return types annotated
  • ?. / ?? used on all nullable Quickshell model properties
  • Multi-monitor widgets wrapped in Variants { model: Quickshell.screens }
  • Singleton {} used as root in singleton files (not QtObject or Item)
  • PersistentProperties uses reloadableId (not reloadSource)
  • Process.command is a string array
  • PwObjectTracker present when retaining Pipewire node references
  • quickshell --reload shows no parse errors

Common Pitfalls

PitfallSolution
component Foo can't access outer idAdd pragma ComponentBehavior: Bound
reloadSource not found on PersistentPropertiesUse reloadableId
Singleton root is QtObject {}Must use Singleton {} as root element
command is a plain stringMust be an array: ["bash", "-c", "..."]
Widget only appears on one monitorWrap in Variants { model: Quickshell.screens } with screen: modelData
Pipewire nodes go null/disappearAdd PwObjectTracker { objects: [...nodes] }
StdioCollector.text is emptyRead text in onStreamFinished, not right after setting running: true
No LSP / autocomplete in editorCreate empty .qmlls.ini next to shell.qml
State lost on --reloadUse PersistentProperties with reloadableId
Binding loop warningsBreak cycles with explicit onXxxChanged handlers instead of two-way bindings
HoverHandler.containsMouse silently never firesHoverHandler exposes hovered (and onHoveredChanged). containsMouse belongs to MouseArea — both compile, only one works per type
QsMenuAnchor.open() fails with "not started in QApplication mode"Add //@ pragma UseQApplication at top of shell.qml and restart QuickShell (not --reload)
Tray item .menu.open() does nothingSystemTrayItem.menu is a QsMenuHandle, not a widget. Display it via a QsMenuAnchor with menu, anchor.item, anchor.edges (then open())

More Info

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.