Files
quickshell_bar/CLAUDE.md
Asmir A eecf1a1b6a Add top-processes drawer to the CPU widget
Clicking the CPU pill opens a popup listing the highest-CPU processes,
refreshed live while open. services/topproc.sh computes top-style per-process
CPU% and mem% from two /proc snapshots and prints one ranked frame per run;
SysStats streams it only while procPollEnabled (drawer open), so the bar pays
nothing at rest.

CLAUDE.md documents the architecture and the runtime constraints discovered
here (helper scripts must be git-tracked for `nix run`; single-shot + exit to
flush stdout; bash+coreutils only; StdioCollector/Timer don't fire in this
Quickshell build; fixed-size popup to avoid stale-buffer on resize under Sway).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 18:33:48 +02:00

118 lines
7.2 KiB
Markdown

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
A Wayland status bar for Sway/i3, written entirely in **QML** and driven by
[Quickshell](https://quickshell.outfoxxed.me/), packaged with a Nix flake.
## Commands
```sh
# run without building (hot-reloads on file save)
nix develop # shell with quickshell + qmlls/qmlformat on PATH
quickshell --path . # or: qs -p .
nix run . # run straight from the flake
nix build . && ./result/bin/quickshell-bar # build the wrapper, then run
```
There is **no test suite, no linter, and no build step** for the QML itself —
it's interpreted and hot-reloaded. The only "build" is the Nix wrapper that
pins fonts and runtime tools. `qmlformat`/`qmlls` (from `qt6.qtdeclarative`,
available in the dev shell) are the only static tooling. Verify changes by
running the bar and looking at it.
## Architecture
**Entry point.** `shell.qml``ShellRoot` with `Variants { model: Quickshell.screens }`
spawns one `widgets/Bar.qml` (`PanelWindow`) per monitor. `Bar.qml` is a fixed
three-zone layout: workspaces (left), clock (center), a `RowLayout` of metric
modules (right). Changing module order/presence is purely editing that RowLayout.
**Three import roots, registered via `qmldir`:**
- `config/``Theme` and `Icons`, both **singletons** (`pragma Singleton` +
`singleton` line in `config/qmldir`). Global palette/geometry/fonts and Nerd
Font glyphs. Import with `import "../config"`, reference as `Theme.x` / `Icons.x`.
- `services/``SysStats`, a **singleton** that is the single source of truth
for all `/proc` + `/sys` metrics. Import with `import "../services"`.
- `widgets/` — the visual modules. `Pill.qml` is the rounded container every
module reuses (`default property alias content` lets children sit inside a
centered RowLayout); `MetricPill.qml` adds the icon + value convention.
**The metrics pipeline (`services/SysStats.qml`) is the core design.** One `Timer`
(1 Hz, `interval` property) calls `_tickOnce()`, which `reload()`s a set of
`FileView`s and parses them into reactive properties (`cpu`, `cpuFreq`, `mem`,
`temp`, rates, battery, …). Widgets just bind to those properties. Key conventions:
- `FileView { blockLoading: true }` makes `reload()` synchronous so `text()` is
fresh on the same tick. Use this for tiny virtual files in `/proc`/`/sys`.
- Add a metric by: declaring a property, adding a `FileView` (or reusing one by
reassigning its `path`, as `_parseNet` does for the active interface), writing
a `_parseX()`, and calling `reload()` + `_parseX()` inside `_tickOnce()`.
- Deltas (CPU%, net rates) keep a `_prev*` field and diff against the last tick.
- **Three things escape the pure-virtual-file rule:** disk usage runs `df` via a
`Process` only every 30 ticks (`_tick % 30`); a one-shot `Process` runs
`services/discover.sh` at startup; and the top-processes list (`topProcs`) is
fed by `services/topproc.sh`, re-run for each frame only while `procPollEnabled`
(true while the CPU drawer is open) — at rest the bar still spawns nothing. The
bar avoids per-tick subprocess spawns by design — keep new always-on metrics
reading virtual files, not shelling out, and gate anything heavier behind an
opt-in flag like `procPollEnabled`.
- **`topproc.sh` does the work, QML just displays.** Each run takes two `/proc`
snapshots `procInterval` apart, computes top-style per-process CPU% and mem% in
the shell, prints one ranked frame of `P <cpu> <mem> <name>` lines, and
**exits**. `SysStats` accumulates stdout via a `SplitParser`, parses it in
`_parseProcFrame` on `onExited`, then re-arms (`Qt.callLater(running=true)`)
while the drawer stays open. Hard-won constraints — violate any one and you get
silent empty frames / a stuck "sampling…":
- **The helper script must be tracked by git.** `nix run` builds from the flake
snapshot (`${self}`), which excludes untracked files (`git status` will warn
"tree is dirty") — an untracked script simply isn't found at runtime. For
iterative work prefer `quickshell --path .`, which reads the working dir
(untracked files included) and hot-reloads.
- **Single-shot + exit, not a long-running loop.** A looping script's output
stuck in bash's block buffer (pipe stdout is block-buffered), so the QML side
never saw a frame; exiting per run forces a flush.
- **bash builtins + coreutils only** (`nproc`/`sort`/`head`/`sleep`/`cat`). The
flake wrapper's `runtimeInputs` is just `bash` + `coreutils`, so `awk` and
`getconf` are absent at runtime.
- **`StdioCollector.streamFinished` never fired** in this Quickshell build, and
a `Timer`-driven re-arm didn't repeat — but `Process.onStarted`/`onExited` and
`SplitParser.onRead` all work (`discover.sh` is the reference). Invoke helpers
as `["bash", Quickshell.shellPath("services/x.sh")]`.
**`discover.sh` handshake.** At startup it prints `TEMP <path>` and `BAT <path>`
(hwmon temp sensor + main battery, skipping `scope=Device` peripherals) on stdout;
the QML side parses those lines into `tempPath`/`batPath`, which then feed
`FileView`s. sysfs paths are stable for a boot, so this runs once, not per tick.
## Conventions & gotchas
- **Icons are referenced by Unicode codepoint** (`config/Icons.qml`,
`String.fromCharCode(0x...)`) so source stays ASCII. The glyph **must exist in
the bundled Nerd Font** — `flake.nix` bundles `nerd-fonts.jetbrains-mono` +
`nerd-fonts.symbols-only` (Font Awesome 4 range, plus Material Design). Font
Awesome 5+ codepoints are NOT present and render as tofu squares; verify a
codepoint is covered before using it. Codepoints above `0xFFFF` (e.g. Material
Design `0xf0xxx`) need `String.fromCodePoint`, not `String.fromCharCode`.
- **Fonts are pinned via `FONTCONFIG_FILE`** in the flake wrapper, so glyphs
render regardless of the host's installed fonts. Adding a font means editing
`fontsConf` in `flake.nix`.
- **`Bar.qml` remaps its layer surface on geometry change** (`geomKey`
toggle `visible`). This is a deliberate workaround for stale-buffer garbage
after output rotation/transform under Sway — don't remove it.
- **Quickshell upstream is tracked directly** in `flake.nix` (git, not nixpkgs)
to pick up layer-surface fixes ahead of release.
- Native Quickshell services back some modules: `Quickshell.I3` (workspaces),
`Quickshell.Services.SystemTray` (tray), PipeWire (volume).
- **Making a module interactive (popups/drawers):** wrap the module's `Pill` in
an `Item` (forward `implicitWidth`/`implicitHeight` from the pill so the bar's
RowLayout still sizes it), add a `MouseArea`, and open a `Quickshell.PopupWindow`;
`CpuGraph.qml` is the reference for a full drawer. Anchor it with
`anchor.item: <the wrapper Item>` (**not** `anchor.window` — the module is
nested in `Bar.qml`'s RowLayout, so its coordinates aren't window coordinates,
and a window-relative rect lands in the screen corner). `anchor.item` makes the
rect item-relative and derives the window automatically; position the dropdown
with `anchor.edges`/`anchor.gravity` (`Edges.*`) and let Quickshell slide it
on-screen. Adding a `MouseArea` directly to a `Pill`'s default content would
place it in the layout — wrapping in an `Item` avoids that.