Files
homelab/dotfiles/quickshell/CLAUDE.md
T
darmanandClaude Opus 5 f3dddc9150 feat(terra): replace tuigreet with a quickshell greeter
greetd now runs a throwaway Hyprland hosting dotfiles/quickshell/greeter.qml,
configured per host via homelab.greeter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 23:34:10 +02:00

137 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
A [Quickshell](https://quickshell.org/) 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 add`ed 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`.
```sh
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:
```sh
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;
`Widgets/Launcher/` is the primary application launcher (`SUPER_L`);
`Widgets/Greeter/` is the greetd login screen, run standalone via `greeter.qml`
(see `services/desktop/quickshell-greeter.nix`), NOT part of `shell.qml`;
`Theme/Theme.qml` is this tree's palette singleton. `DebugWindow.qml` stages a
single widget on the secondary monitor for eyeballing it in isolation.
The prompt and the launcher are MODALS: each raises the shared scrim, lands
on the focused monitor, and takes the keyboard off the rail. That is why the
shell instantiates them rather than `shell.qml` — see `modalOpen` there, which
is the one place a new modal has to be named.
- `widgets/bar/``DenseBar` and `StatusBarPanel`, the rail's predecessor. Not
instantiated by `shell.qml` any more; `StatusBarPanel` is still used by the
remaining launcher variants.
- `widgets/launcher/` — shared `AppModel` search/execution plus the ten launcher
variants still under evaluation, on `SUPER CTRL 111`. Variant 8 has moved to
`HyprChrome/Widgets/Launcher/`; `AppModel.qml` is duplicated there so the
HyprChrome tree stands alone, and this copy goes when the variants do.
- `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.