135 lines
8.5 KiB
Markdown
135 lines
8.5 KiB
Markdown
# 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`);
|
||
`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 1–11`. 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.
|