Files
homelab/dotfiles/quickshell/CLAUDE.md
T
darmanandClaude Opus 5 27b924f5e9 docs(quickshell): correct CLAUDE.md's config-path and architecture claims
The file said ~/.config/quickshell is a symlink to this repo and that saving a
file hot-reloads the running shell. Both are wrong and cost real debugging
time: hosts/terra/home.nix ships the tree with xdg.configFile."quickshell",
which COPIES it into the store, so the config dir is a read-only symlink into
/nix/store and the running shell watches the frozen copy. Hot reload only
applies to a working-tree instance, which is what `nix develop` + qs-dev is
for.

Also records the trap that follows from it: a new file must be git-added
before it can be deployed at all, because flakes read the git tree and drop
untracked files silently — producing a deployed config that fails to load,
which does not surface until the next restart because the running shell keeps
serving the store path it resolved at launch.

Architecture section was stale independently of that: shell.qml has not
instantiated Bar/BarTop/BarBottom for a while, there are eleven launcher
variants rather than eight, the HyprChrome tree was undocumented, and there
are TWO Theme singletons — a token added to one does not exist in the other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GAq2kKCLazZmrKvkd3akud
2026-09-01 23:38:09 +02:00

8.2 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this is

A Quickshell configuration — a QML-based Wayland desktop shell (bar, launcher, tray, decorations) for a Hyprland/wlroots setup. shell.qml is the entry point.

Running / testing changes

~/.config/quickshell is NOT a symlink to this repo. hosts/terra/home.nix ships the tree with xdg.configFile."quickshell", which COPIES it into the nix store, so the config directory is a read-only symlink into /nix/store/.... Editing a file here therefore changes nothing about the running shell: it is watching the frozen store copy, and every edit would otherwise cost a nixos-rebuild. Two consequences worth knowing before debugging anything:

  • A new file must be git added before it can be deployed at all. Flakes read the git tree, and untracked files are silently dropped — with no warning and no eval error. An untracked module that shell.qml imports produces a deployed config that fails to load, which does not surface until the next restart because the running shell keeps serving the store path it resolved at launch.
  • To check what a deploy actually shipped, compare the evaluated source with what is live: nix eval --raw '.#nixosConfigurations.terra.config.home-manager.users.darman.xdg.configFile."quickshell".source' then ls that path against ls -l ~/.config/quickshell.
nix develop        # then: qs-dev   — swap the running shell for the WORKING TREE, no rebuild
qs                 # runs the packaged (store) config
qs -p .            # run this directory explicitly
qs -n              # exit immediately if another instance is already running
qs kill            # kill the default-config instance ('qs kill -p <path>' for a working-tree one)

qs-dev is the edit-save-see loop: it starts a working-tree instance, waits until it is confirmed up, and only then kills the packaged one, so a QML error leaves you on your normal bar instead of no bar. It is a SWAP rather than a second instance because quickshell keys instance identity on the config path — two instances would both map layer-shell bars onto every output. See the nix develop block in flake.nix.

Pointed at the working tree, quickshell hot-reloads on file save. It watches file CONTENT: touch alone never reloads (mtime is not a change), while any real edit does, including inode-replacing ones (sed -i, perl -i). A save that is not picked up leaves the shell rendering the previous config with no error — qs log shows a Reloading configuration... line for every save it saw, so that is the check.

A single component can also be run in isolation, which is the way to exercise something that owns a service or a surface without bringing up the whole rail: point qs -p at a scratch directory whose shell.qml instantiates only that component, with the repo's directories symlinked in for the qs. imports.

There is no build/lint/test tooling wired up in this repo, but two things are worth reaching for. qmllint (from qtdeclarative) catches syntax and binding errors without a compositor — expect noise from the synthesized qs.* modules and the Theme singleton, which it cannot resolve:

qmllint -I <qtdeclarative>/lib/qt-6/qml -I <quickshell>/lib/qt-6/qml -I . <file>.qml

And tools/quickshell-preview/render.sh renders an Item-rooted component to a PNG offscreen (see tests/). Keep production PanelWindow wrappers thin and put the visuals in an Item so they can go through that path. qmlls is configured via .qmlls.ini for editor diagnostics.

Architecture

shell.qml is a Scope instantiating the top-level pieces as siblings: HyprChromeShell (the status rail), the eleven launcher variants, Notifications, VolumeOsd and Vitals. Each manages its own PanelWindow(s); there is no central layout manager.

The one exception, and the pattern to follow for anything new that needs it, is HyprChromeShell: it owns the state its surfaces have to AGREE on rather than letting each decide — which monitor they live on, the rail's density, whether a polkit prompt is open, and the layer pair. A second reader is what makes a property shell state; the file's own header comment enumerates them and says why each qualifies. Layer levels in particular are derived TOGETHER, because two surfaces on one layer stack by creation order while one layer apart is a guarantee.

Import convention: QML modules are imported by their path under the repo root using the qs. namespace, e.g. import qs.widgets.launcher, import qs.widgets.decoration. Sibling files in the same directory are imported with a relative string import instead (e.g. Bar.qml does import "modules").

Multi-monitor: a surface that must exist on every screen wraps its PanelWindow in Variants { model: Quickshell.screens }, one instance per connected screen; pragma ComponentBehavior: Bound + required property var modelData is the standard pattern for those delegates. A surface that belongs to ONE screen instead takes it as a property from the shell. Note Quickshell.screens is a QML list, not a JS array — no .find() or .filter() on it, hence the index loops in HyprChromeShell.

Directory layout:

  • HyprChrome/ — the current shell. Widgets/HyprChromeShell.qml is the owner described above; Widgets/ChromeBackdrop.qml is the scrim (dim + drafting grid) shared by the rail and the polkit prompt; Widgets/Bar/ holds the rail and its panels, with Bar/Panels/BarPanel.qml the chamfered chrome they all extend; Widgets/Polkit/ is the authentication agent and its dialog; Theme/Theme.qml is this tree's palette singleton. DebugWindow.qml stages a single widget on the secondary monitor for eyeballing it in isolation.
  • widgets/bar/DenseBar and StatusBarPanel, the rail's predecessor. Not instantiated by shell.qml any more; StatusBarPanel is still used by the launchers.
  • widgets/launcher/ — shared AppModel search/execution plus eleven launcher variants. ApplicationLauncher (variant 8, and the primary SUPER launcher) keeps its visual core in the headlessly renderable ApplicationLauncherContent; the rest are available on SUPER CTRL 111 for comparison.
  • widgets/decoration/ — reusable QtQuick Shape-based visual accents (angled panel edges, slashes) used to give bar panels their non-rectangular look. Dummy.qml is a placeholder/test rectangle.
  • widgets/input/ — thin wrappers around QtQuick.Controls inputs (currently just TextField).
  • widgets/layout/HorizontalStack/VerticalStack: RowLayout/ColumnLayout wrappers that expose default property alias content for terser call sites, with a trailing filler Item that soaks up remaining space.
  • assets/ — SVG icons referenced via file://${Quickshell.shellDir}/assets/....

Styling: All colors and font families come from a Theme singleton — there are no color or font literals left anywhere under widgets/. There are TWO, carrying the same palette for the two trees: widgets/theme/Theme.qml (import qs.widgets.theme) and HyprChrome/Theme/Theme.qml (import qs.HyprChrome.Theme). Match the one your file's tree already uses; a token added to one does not exist in the other. Add a token rather than hardcoding a value; alpha variants of the two main colors go through Theme.textAlpha(a) / Theme.accentAlpha(a) instead of a hand-written Qt.rgba(...). Metrics (sizes, spacing) are still per-component.

Component base pattern: BarWidget.qml (widgets/bar/modules/BarWidget.qml) is a WrapperMouseArea + WrapperRectangle combo providing hover-triggered border highlight (Behavior on border.color animation) and Layout.fillWidth. Bar modules extend it via default property alias content rather than duplicating the hover/border chrome.

Fonts: Two families, both via Theme. Theme.displayFont / Theme.readoutFont (an alias of it) are DepartureMono Nerd Font, installed by services/desktop/desktop-apps.nix; Theme.microFont is DejaVu Sans Mono. The shell no longer uses Digital-7 Mono, which was never packaged and depended on a manual ~/.dots/fonts/digital_7 install.