- Dock drag-to-reorder for pinned apps (insertion gap, quick-slide vs dwell) - Fullscreen detection: scan all windows via HyprlandData.windowList - IdleCoordinator, GlobalStates, Session.qml updates - Deploy script, qmldir, settings, wallpaper, visualizer fixes - sessiond server, memory module updates Co-Authored-By: Claude <noreply@anthropic.com>
7 KiB
quickshell-idle-power — lazy-load panels + consolidate polling processes
Status: 2026-07-21 — scoped, not started. Two quick wins already landed separately (see "Already done" below); this doc covers the two larger sweeps (#3 LazyLoader coverage, #4 process consolidation) to be worked tomorrow.
Config: qs -c souveraine → symlinks into ~/souveraine-surfaces/quickshell/
(the ~/.config/quickshell/souveraine/ tree is symlinks; edit the real tree).
Quickshell 0.3.0. Composed of three layers:
modules/ services/ panelFamilies/→ souveraine (owned, edit freely)ii-base/→ borrowed ii/Caelestia tree (treat as replaceable; keep coupling to souveraine singletons OUT of here)ii-phone/,ii-previous/→ phone snapshot / previous deploy
Why (measured)
At a 49-min uptime, qs had burned 149 CPU-sec and held 560 MB RSS
(15% of RAM) on the Pixel 3 — second only to the active claude session. The
display idle layer (services/IdleCoordinator.qml) is already excellent
(state machine Active→Dimmed→Lock→Suspend→Asleep→Waking, native IdleMonitor
with respectInhibitors, brightness dim, hypridle screen-off). The gap is that
the idle machine dims the screen but does not quiet the widgets — timers
tick and processes poll regardless of visibility. These two sweeps attack the
steady-state CPU (over-eager construction) and RSS (per-widget processes).
Quickshell FAQ guidance backing this:
- "The main thing you can do to reduce memory usage is to use Loaders."
- Loader for Item-derived components; LazyLoader otherwise.
- "Using a process per widget will use significantly more memory than using a single process." Ref: https://quickshell.org/docs/v0.3.0/guide/faq/
Already done (2026-07-21, do NOT redo)
ii-base/services/DateTime.qml— uptime Timer wasinterval: 10(reloading /proc/uptime off disk 100×/sec). Changed tointerval: 60000+triggeredOnStart: true. The clock itself isSystemClock(event-driven), was never the problem.ii-base/modules/ii/background/widgets/visualizer/VisualizerWidget.qml— the wave visualizer's 33ms (30fps) repaint Timer wasrunning: root.style === "wave"; nowrunning: parent.visibleso it stops repainting when the canvas isn't shown. (Considered aWindow.visibilitygate too, but that needsimport QtQuick.Windowin the borrowed tree — deferred;parent.visibleis dependency-free and sufficient.)
Both verified: qs hot-reloaded without QML errors, pid stayed alive.
Task 3 — Lazy-load the heavy panels (RSS lever)
Current: 9 LazyLoader vs 123 Loader across 683 QML files → a lot is
eagerly constructed at startup. Top-level panel modules under
ii-base/modules/ii/ that are candidates (not visible at boot):
cheatsheet overlay overview sidebarLeft sidebarRight wallpaperSelector
sessionScreen screenTranslator regionSelector mediaControls polkit
notificationPopup onScreenKeyboard dock
Do:
- For each panel not shown at boot, wrap its instantiation in a
LazyLoader(non-Item / window-like → LazyLoader; Item-derived → Loader). Load on the signal/state that reveals it (e.g. sidebar open toggle, GlobalStates flag), unload after. - Especially: the anime/booru sidebar (
ii-base/modules/ii/sidebarLeft/anime/) and cheatsheet — heavy, rarely open. - Verify each panel still opens with no first-open UI-thread stall (LazyLoader loads sync if shown before background load finishes — see LazyLoader docs).
Watch: keep LazyLoader wrapping in the layer that OWNS the panel. Don't add souveraine-singleton dependencies into ii-base while doing this.
Measure before/after: ps -eo pid,pmem,rss,comm | grep qs (RSS) at a fresh
launch, before opening any panel.
Progress 2026-07-22 (task 4 partially landed)
Landed live + promoted to the laptop repo (souveraine tree):
- GlobalStates.qml (souveraine override): new bool — the shared cross-layer idle surface the doc prescribed.
- services/IdleCoordinator.qml: publishes displayActive on every state transition (Active/Waking = true).
- ii-base/services/ResourceUsage.qml + NetworkTraffic.qml: repeat pollers gated (+ ).
- VisualizerWidget.qml: cava watchdog gated on , cava torn down when either goes false.
- SystemInfo.qml inspected: its timers are one-shot startup probes, left alone. Notifications.qml: no repeat poller found, left alone.
One hot-reload during editing tripped the known "lockscreen surfaces without active lock" FATAL (reload race with sessiond, not a QML error); the service restart loaded clean. CPU-delta measurement while locked still pending (baseline at /tmp/qs-idle-baseline-*).
Task 3 note from code reading: most panels ALREADY gate content internally on GlobalStates.*Open via Loaders; their IpcHandlers live inside the panel files, so adding at the PanelLoader level would unload the very handler that opens them — do NOT do that. The real task-3 work is converting the -gated panels (Overview line 23, SidebarLeft lines 89/181) to internal Loaders, one panel at a time. Overview deliberately deferred until the 2026-07-21 search-fix has device acceptance.
Task 4 — Consolidate per-widget processes (CPU + RSS lever)
Current: ~48 running: true Process instances (souveraine 14, ii-base 32,
ii-phone 2). FAQ: one process feeding many widgets ≫ cheaper than N processes.
Do:
- Audit
ii-base/services/*.qmlfor multiple widgets independently spawning processes for the SAME data. Known pollers to review: ResourceUsage.qml, NetworkTraffic.qml, SystemInfo.qml, Notifications.qml, plus /proc readers. Route each data source through ONE Singleton service that widgets bind to (theservices/layer already does this well — the duplication lives in the borrowed ii-base tree). - Gate long-poll services on idle state where a stale value is fine while
dimmed/locked: e.g.
running: IdleCoordinator.state === IdleCoordinator.Activefor background stats/resources/network widgets. NOTE: IdleCoordinator is a souveraine singleton — if gating an ii-base widget on it, do the gating in a souveraine wrapper/override, not by importing it into ii-base. Alternatively expose an idle bool via the existingGlobalStates/config surface that both layers already share. - The cava process behind the visualizer (VisualizerWidget.qml watchdog, 2s) is a good candidate to also stop when the background isn't visible.
Measure before/after: cumulative CPU: ps -eo pid,times,comm | grep qs
after a fixed idle interval (e.g. leave locked 10 min, compare CPU-sec delta).
Acceptance
- Fresh-launch RSS meaningfully below ~560 MB.
qsCPU-sec growth while the screen is off/locked approaches zero (today it keeps climbing from always-on timers/pollers).- No panel regressions: every lazy-loaded panel opens without a stall; every consolidated service still updates its widgets live when Active.