diff --git a/dotfiles/quickshell/CLAUDE.md b/dotfiles/quickshell/CLAUDE.md index c840656..1668082 100644 --- a/dotfiles/quickshell/CLAUDE.md +++ b/dotfiles/quickshell/CLAUDE.md @@ -4,38 +4,122 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## What this is -A [Quickshell](https://quickshell.org/) configuration — a QML-based Wayland desktop shell (bar, launcher, tray, decorations) for a Hyprland/wlroots setup. `~/.config/quickshell` is a symlink to this repo, so Quickshell loads `shell.qml` here as the "default" config. +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 -qs # runs ~/.config/quickshell/shell.qml (this repo, since it's the symlinked default config) -qs -p . # run this directory explicitly regardless of symlink -qs -n # exit immediately if another instance is already running (use to avoid duplicate shells while iterating) +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 ' for a working-tree one) ``` -Quickshell hot-reloads QML on file save when already running, so for most edits just save and check the running instance rather than restarting `qs`. 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. There is no separate build/lint/test tooling in this repo — verification is visual/behavioral via the running shell. `qmlls` (QML language server) is configured via `.qmlls.ini` for editor diagnostics. +`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 /lib/qt-6/qml -I /lib/qt-6/qml -I . .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 the entry point: a `Scope` that instantiates the top-level pieces — `Bar`, `BarBottom`, `BarTop`, and a hidden `Launcher` — as siblings. Each top-level widget manages its own `PanelWindow`(s); there's no central layout manager. +`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**: Bar/BarTop/BarBottom each wrap their `PanelWindow` in `Variants { model: Quickshell.screens }`, so one window instance is created per connected screen. `pragma ComponentBehavior: Bound` + `required property var modelData` is the standard pattern for these per-screen delegates. +**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**: -- `widgets/bar/` — `Bar.qml` is the main sidebar (right-anchored, full height) hosting the module stack (date, clock, tray, decorative dividers); `BarTop.qml`/`BarBottom.qml` are thin accent-colored strips anchored to the top/bottom edges. -- `widgets/bar/modules/` — individual bar widgets (`Clock`, `Date`, `Tray`/`TrayItem`, `Volume`) built on the shared `BarWidget` base component. -- `widgets/launcher/` — shared `AppModel` search/execution plus eight launcher variants. `ApplicationLauncher` (variant 8 and the primary `SUPER` launcher) keeps its visual core in the headlessly renderable `ApplicationLauncherContent`; variants 1–7 remain available on `SUPER CTRL 1–7` for comparison. +- `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 1–11` 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 the `Theme` singleton -(`widgets/theme/Theme.qml`, `import qs.widgets.theme`) — there are no color or -font literals left anywhere under `widgets/`. Add a token there rather than +**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.