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

7.2 KiB

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, packaged with a Nix flake.

Commands

# 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.qmlShellRoot 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 FileViews 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 FileViews. 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 Fontflake.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.