plugins/linux-hyprland/skills/quickshell/SKILL.md
Use this skill when writing, reviewing, or debugging Quickshell configurations (QML files for desktop shell UI on Wayland/Hyprland). Triggers on: QML files with Quickshell imports, shell.qml entry points, PanelWindow or FloatingWindow usage, Quickshell service integration (PipeWire, MPRIS, notifications, Hyprland IPC), Wayland layer-shell or session-lock code, custom bar/panel/widget/dock/OSD/lockscreen/launcher development, or any question about building a desktop shell with Quickshell on Hyprland.
npx skillsauth add rbozydar/rbw-claude-code quickshellInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quickshell is a Qt6/QML desktop shell toolkit that Claude does not have reliable training data for. This skill provides the complete API reference, architectural patterns, gotchas, and pointers to local reference repositories needed to write correct Quickshell QML. Without it, Claude will hallucinate Quickshell types, confuse it with other shell frameworks, or miss critical patterns like PwObjectTracker and the Variants multi-monitor pattern.
Build desktop shells (bars, panels, docks, lockscreens, launchers, OSDs, dashboards, notification daemons) using the Quickshell framework on Wayland compositors, primarily Hyprland.
Quickshell is a Qt6/QML-based shell toolkit. You write declarative QML that Quickshell renders as Wayland surfaces (panels, overlays, floating windows). It live-reloads on file save. Config lives at ~/.config/quickshell/shell.qml by default.
Key differentiators from other shell frameworks (AGS, EWW):
Every shell starts with a ShellRoot in shell.qml:
import Quickshell
ShellRoot {
// Non-visual root container for all shell objects
// settings.watchFiles: true — enables live reload on file change
}
| Type | Use | Key Properties |
|------|-----|----------------|
| PanelWindow | Bars, panels, widgets anchored to screen edges | anchors.{top,bottom,left,right}, height/width, exclusiveZone, screen |
| FloatingWindow | Standard desktop windows, settings UIs | Standard Qt window properties |
| WlSessionLockSurface | Lock screen surfaces | Used inside WlSessionLock |
Always use Variants with Quickshell.screens for per-monitor windows:
Variants {
model: Quickshell.screens
PanelWindow {
property var modelData
screen: modelData
anchors { top: true; left: true; right: true }
height: 30
}
}
This is reactive — windows create/destroy as monitors connect/disconnect. Never hardcode screens.
Scope — groups non-visual children (Process, Timer, Connections). Use when extracting components to separate filesSingleton (with pragma Singleton) — global shared state accessible from any file. Use for services and shared dataShellRoot — the outermost Scope; properties defined here are accessible without an id from nested scopes~/.config/quickshell/
├── shell.qml # Entry point (ShellRoot)
├── modules/ # Major UI subsystems (bar/, dashboard/, lock/, etc.)
├── components/ # Reusable UI components
├── services/ # Singleton services (Audio, Network, Hypr, etc.)
├── config/ # Configuration system
└── utils/ # Utility singletons
Uppercase QML filenames become types automatically. Bar.qml becomes Bar {}. Import subdirectories with import "modules/bar".
See references/qml-patterns.md for the complete pattern library with code examples.
Key patterns:
text: Time.time auto-updates when Time.time changesConnections { target: X; function onSignal() {...} } or arrow: onRead: data => clock.text = dataLazyLoader { active: condition; PanelWindow { ... } } for memory-efficient ephemeral UIProcess { command: ["cmd"]; stdout: SplitParser { onRead: data => prop = data } }Timer { interval: 1000; running: true; repeat: true; onTriggered: ... }required property PwLinkGroup modelData in Repeater delegatessink?.audio?.volume ?? 0 for nullable service objectsmask: Region {} makes a window transparent to inputSee references/modules-api.md for the full module reference with all types and properties.
import Quickshell.Hyprland
// All reactive — auto-update on compositor changes
Hyprland.monitors // ObjectModel<HyprlandMonitor>
Hyprland.workspaces // ObjectModel<HyprlandWorkspace>
Hyprland.toplevels // ObjectModel<HyprlandToplevel>
Hyprland.focusedMonitor // HyprlandMonitor
Hyprland.focusedWorkspace // HyprlandWorkspace
Hyprland.activeToplevel // HyprlandToplevel
// Dispatch commands
Hyprland.dispatch("workspace 3")
Hyprland.dispatch("movetoworkspace 5")
PanelWindow {
WlrLayershell.layer: WlrLayer.Top // Top, Bottom, Overlay, Background
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive // for modals
exclusiveZone: 0 // 0 = don't reserve space
exclusionMode: ExclusionMode.Ignore // ignore other panels' zones
}
HyprlandFocusGrab {
id: focusGrab
active: drawerVisible
windows: [drawerWindow]
onCleared: drawerVisible = false // clicked outside
}
Two mechanisms to wire Hyprland keybinds to shell actions:
1. Native global shortcuts (preferred, lower latency):
# hyprland.conf
bind = Super, A, global, quickshell:sidebarToggle
bind = Super, Tab, global, quickshell:overviewToggle
Maps to CustomShortcut { name: "sidebarToggle"; onPressed: ... } in QML.
2. IPC via CLI (resilient, works across instances):
bind = , XF86MonBrightnessUp, exec, qs ipc call brightness increment
bind = Super, Super_L, exec, qs ipc call launcher toggle
Define targets in QML with IpcHandler:
IpcHandler {
target: "brightness"
function increment(): void { ... }
function decrement(): void { ... }
function get(): real { return currentBrightness }
}
CLI usage: qs ipc show (list targets), qs ipc call <target> <func> [args]
Use DesktopEntry.command (not .execString) with Quickshell.execDetached():
function launch(entry: DesktopEntry): void {
Quickshell.execDetached({
command: entry.command,
workingDirectory: entry.workingDirectory
});
}
SystemClock fires within ±50ms of actual clock tick. new Date() can be off by up to 1 second:
SystemClock {
id: clock
precision: SystemClock.Seconds // or .Minutes
}
// Use clock.date, clock.hours, clock.minutes, clock.seconds
// Format: Qt.formatDateTime(clock.date, "hh:mm")
Real shells use JsonAdapter + FileView for persistent user config:
pragma Singleton
import Quickshell
import Quickshell.Io
Singleton {
property alias appearance: adapter.appearance
FileView {
id: configFile
path: Paths.config + "/shell.json"
watchChanges: true
}
JsonAdapter {
id: adapter
source: configFile
}
function save(): void { saveTimer.restart() }
Timer {
id: saveTimer
interval: 500
onTriggered: configFile.write(JSON.stringify(adapter.serialize(), null, 2))
}
}
// services/Audio.qml
pragma Singleton
import Quickshell
import Quickshell.Services.Pipewire
Singleton {
readonly property PwNode sink: Pipewire.defaultAudioSink
readonly property bool muted: !!sink?.audio?.muted
readonly property real volume: sink?.audio?.volume ?? 0
function setVolume(v: real): void {
if (sink?.ready && sink?.audio) {
sink.audio.muted = false;
sink.audio.volume = Math.max(0, Math.min(1.5, v));
}
}
}
references/qml-patterns.md — Complete QML pattern library with code examples for every common shell pattern. Read when implementing specific features.references/modules-api.md — Full Quickshell module/type reference. Read when you need the exact API for a module.references/reference-repos.md — Map of all local reference repositories with paths and what to look for in each. Read when searching for real-world implementation examples.references/best-practices.md — Architecture patterns for scalable shells: theming, design tokens, error handling, logging, performance, property conventions. Read when starting a new shell or reviewing architecture.references/feature-matrix.md — Feature-by-feature comparison across all 4 reference shells (Caelestia, DMS, illogical-impulse, Noctalia) with exact file paths. Read when you want to implement a specific feature and need to find a reference implementation.references/visual-effects.md — Qt visual effects guide: MultiEffect (blur, shadow, mask, colorize), ShaderEffect (custom GLSL), layer system, Canvas, compositor-level blur. Read when implementing any visual effect.references/qt-essentials.md — Core Qt/QML patterns: focus management, scrollable panels, states/transitions, color manipulation, gradients, utility functions, animations. Read for Qt fundamentals that aren't Quickshell-specific.id: clock inside a Variants delegate cannot be referenced from outside. Define a property on a parent Scope/ShellRoot and bind to it. Error: ReferenceError: clock is not defined.
You must bind PwObjectTracker { objects: [Pipewire.defaultAudioSink] } before accessing .audio.volume on a PwNode. Without it, properties won't update reactively.
time: time inside a component binds time to itself. Use an id: time: root.time.
Quickshell singletons must use Singleton {} as root (not QtObject or Item). This ensures proper reload behavior.
A PanelWindow { screen: someSpecificScreen } without Variants will crash when that screen disconnects. Always use Variants + Quickshell.screens.
By default, PanelWindow reserves screen space. For overlays, OSDs, and popups that shouldn't push windows, set exclusiveZone: 0.
Background < Bottom < Top < Overlay. Bars use Top, lock screens use Overlay with exclusive keyboard focus. Wrong layer = UI renders behind other surfaces.
command: "date +%H:%M" fails. Use command: ["date", "+%H:%M"] or command: ["sh", "-c", "date +%H:%M"] for shell features.
External edits trigger onChanged which can re-trigger save logic. Use a debounce timer and a recentlySaved flag to break the loop.
Quickshell.env("VAR") reads at startup only. Not reactive to later environment changes.
Hyprland.workspaces is an ObjectModel, not a JS array. To use .filter(), .map(), .find(), .findIndex() you must call .values first:
// WRONG: Hyprland.workspaces.filter(...)
// RIGHT:
Hyprland.workspaces.values.filter(w => !w.name.startsWith("special:"))
Some Hyprland data is only accessible through .lastIpcObject (the raw JSON snapshot), not as direct QML properties:
workspace.lastIpcObject.windows // window count
monitor.lastIpcObject.specialWorkspace.name
File changes, D-Bus signals, and subprocess output often arrive before data stabilizes. Use a short Timer delay (20-100ms) before processing:
Connections {
target: someExternalSignal
function onChanged() { delayTimer.restart() }
}
Timer {
id: delayTimer
interval: 50
onTriggered: actuallyProcessTheData()
}
In ListView with remove transitions, modelData is destroyed immediately when the item is removed from the model — before the exit animation completes. Cache needed values in local properties during Component.onCompleted:
property int cachedId
Component.onCompleted: cachedId = modelData.id
A Behavior on width { NumberAnimation {} } fires immediately when the component loads, causing jumpy animations. Start with enabled: false and enable via a Timer after Component.onCompleted.
ScriptModel only works with unique values. Duplicate values cause undefined behavior.
If a window's color is opaque before becoming visible, it cannot later become transparent unless surfaceFormat.opaque is explicitly set to false.
PersistentProperties { reloadableId: "myId" } preserves state across reloads, but the ID must be unique across the entire shell. Duplicate IDs cause silent state corruption.
Set in the root shell.qml before imports:
//@ pragma Env QS_NO_RELOAD_POPUP=1 // suppress default reload popup
//@ pragma Env QSG_RENDER_LOOP=threaded // threaded rendering
//@ pragma ShellId myshell // stable shell identity
//@ pragma RespectSystemStyle // allow QT_QUICK_CONTROLS_STYLE
../../foo.png style references fail. All files must be inside the shell directory or use absolute paths.
import qs.path.to.module for root-relative importsThe old "root:/" syntax is replaced. import qs.modules.bar resolves to modules/bar/ relative to the shell root. This also improves qmlls support.
Quickshell auto-discovers QML types. Don't create qmldir manifests — they're not used. Module structure is handled by directory layout and import qs. paths.
Variants does not support async loading. If placed inside a LazyLoader, it blocks until all instances are created. For heavy content, use Loader with asynchronous: true instead.
Config files, IPC responses, and external data can be malformed. Always wrap JSON.parse() in try/catch with a fallback:
try { config = JSON.parse(fileView.text()) }
catch (e) { config = {} }
Calling .destroy() on objects during a reload can throw. Wrap in try/catch:
try { obj.destroy() } catch (_) {}
root.list.push(item) does NOT trigger reactive updates. You must reassign:
root.list = [newItem, ...root.list] // triggers change
Enable for a shell config by creating an empty .qmlls.ini next to shell.qml:
touch ~/.config/quickshell/.qmlls.ini
Quickshell auto-populates it with import paths on next run. Gitignore this file (machine-specific).
Editor setup:
require("lspconfig").qmlls.setup {} + :TSInstall qmljslsp-mode or eglot with qml-ts-mode (tree-sitter grammar: yuja/tree-sitter-qml)qt-qml.qmlls.useQmlImportPathEnvVarqmlls caveats:
PanelWindow in particular cannot be resolvedimport qs.path.to.module (v0.2+) instead of old "root:/" imports for better LSP resolutionShips with Qt6 (qt6-declarative-tools or similar):
qmlformat -i file.qml # format in-place
qmlformat -i **/*.qml # format all QML files
Also from Qt6:
qmllint file.qml
qmllint **/*.qml
Handled by qmlls to the extent possible. No Quickshell-specific type checker exists — rely on qmlls plus runtime error checking via qs -p ..
After writing QML:
qs -p /path/to/config/ (or quickshell -p .) to testqmllint *.qml to catch static issueshyprctl keyword monitor HDMI-A-1,disable then re-enablehyprctl layershyprctl clientspragma Singleton with Singleton {} root typeqs -p .development
This skill should be loaded when writing, reviewing, or refactoring Python code to apply strict coding standards directly in the current context without spawning a subagent. It provides comprehensive Python development standards covering SOLID principles, asyncio patterns, type hints, testing, and production-quality code.
tools
This skill should be used when invoking the Gemini CLI for code review, plan review, or any prompt-based task. It provides correct invocation patterns emphasizing stdin piping and @ syntax over shell variable gymnastics.
development
This skill should be used when thorough, multi-perspective research with citations is needed. It performs comprehensive research using a diffusion research loop with domain specialization, supporting general web research and specialized domains (geopolitical with GDELT). Auto-detects domain from query or accepts an explicit --domain flag.
testing
Generate or improve a company-specific data analysis skill by extracting tribal knowledge from analysts. BOOTSTRAP MODE - Triggers: "Create a data context skill", "Set up data analysis for our warehouse", "Help me create a skill for our database", "Generate a data skill for [company]" → Discovers schemas, asks key questions, generates initial skill with reference files ITERATION MODE - Triggers: "Add context about [domain]", "The skill needs more info about [topic]", "Update the data skill with [metrics/tables/terminology]", "Improve the [domain] reference" → Loads existing skill, asks targeted questions, appends/updates reference files Use when data analysts want Claude to understand their company's specific data warehouse, terminology, metrics definitions, and common query patterns.