Popouts can now be opened by niri keybind and driven entirely without a
mouse — the whole point of the keybinds.
- keybind: PopoutRouter singleton + IpcHandler (target "popout") route
toggle/open/close to the focused monitor's PopoutState via
Niri.activeOutput; each widget registers its popout by name and its
click handler toggles by name. Mod+A now opens the audio popout
(replacing walker's audio menu).
- navigation: shared controls (Toggle/ListRow/VolumeSlider) and the new
IconButton are focusable (activeFocusOnTab, focus ring, Space/Enter,
slider Left/Right); PopoutPanel focuses the first control on open and
maps Up/Down onto Qt's Tab chain; an open popout grabs the keyboard.
- ad-hoc glyph buttons (audio mute, media transport, notif dismiss, BT
forget) folded into IconButton; destructive actions turn red.
Tray stays mouse-only (per-icon menu, not keybind-addressable).
The composition + composable-popouts refactor (5120b77, 411d8b0) left
three rough edges:
- 6 popouts still read the removed PopoutState.currentName in their
`shown` gate (undefined === "x" → permanently false), so wifi scan,
bluetooth discovery, threshold reread, governor refresh, toast-silence
and tray drill-down reset never fired again. Only the current component
is ever instantiated, so `hasCurrent` alone is the correct gate.
- ModuleBox stopped self-hiding when empty: the Battery box on a desktop
(no battery) and an empty tray rendered a bare rounded pill. Gate the
shared container on `inner.implicitWidth > 0` so the fix covers both.
- CLAUDE.md customization example missed `import Quickshell` (unloadable
as written) and called a Row's rejected centerIn anchor "harmless"
(Row warns at runtime and ignores it).
The composition refactor left Popouts.qml a hardcoded switch, so a
user-composed widget could join the bar but couldn't open a popout in
the shared animated overlay — second-class, and a widget without a
popout is useless for most cases. quickshell's native PopupWindow would
be composable but drops the unfold/morph animation; keep the animation,
make the registry composable instead (caelestia's central Panels.qml
registry was considered and rejected — it fits a few author-owned edge
drawers, not many user-composable per-widget popouts).
PopoutState.open() now takes a Component instead of a name; each widget
carries its popout inline and hands it to the shared host, which renders
whatever is current. Popouts.qml is gone — no central registry, a user
widget brings its own popout and it renders in the same morphing frame.
Verified: qmllint clean, and an end-to-end config where a user widget
opens its own popout on startup instantiates with no errors or binding
loops. Visual/animation not machine-verifiable.
The user-extension layer (moonarch.json + UserConfig singleton +
per-widget enabled() gates + UserWidgets slot) was a bespoke graft.
quickshell has no native settings/toggle/override concept — a config
is composable QML, and $XDG_CONFIG_HOME wins over $XDG_CONFIG_DIRS
(verified in the docs and src/launch/command.cpp).
Ship the bar as a named config (/etc/xdg/quickshell/moonarch/, launched
`-c moonarch`) of importable components; the composition (which widgets,
in what order) lives in shell.qml. Users customize by copying shell.qml
to ~/.config/quickshell/moonarch/ (wins by XDG precedence) and editing
it — reorder, add, and replace all fall out of plain composition, which
also retires the deferred reorder/user-popout/override items.
Also fixes the ModuleBox childrenRect binding loop (Item -> Row).
BREAKING CHANGE: ~/.config/quickshell/moonarch.json and UserWidgets.qml
are no longer read; per-widget toggles are gone in favor of composition.
Verified: qmllint clean (only quickshell C++ type-resolution artifacts),
quickshell loads the default config and a cross-dir composed config with
no binding loops or errors.
The quickshell pilot proved out in daily use; promote it from manual
pilot to the session default. Additive — no hard deletes: Waybar and
swaync stay installed as reserves.
- niri spawns `quickshell` instead of `moonarch-waybar`; Alt+W toggles
quickshell ⇄ Waybar for the rare fallback
- swaync's spawn-at-startup is commented out so it releases
org.freedesktop.Notifications for quickshell's own server (verified:
quickshell owns the name; flip the comment to return to swaync)
- quickshell added to packages/official.txt (config ships automatically
via moonarch-git's defaults/xdg install loop)
Remaining: push → CI rebuilds moonarch-git → pacman -Syu; niri changes
take effect on relogin.
quickshell has no native config merge, so the system bar could only be
adopted wholesale (fork it, lose system updates). This adds an in-repo
extension path: users toggle widgets and add their own without forking.
- UserConfig singleton reads ~/.config/quickshell/moonarch.json
({"widgets":{"<key>":false}}); absent/broken = all on, live-reloaded
- qmldir (generated) exposes singletons + components so user QML can
import "file:///etc/xdg/quickshell" and reuse Theme/ModuleBox/…
- Bar gates its 16 widgets on UserConfig.enabled(key) and loads an
optional UserWidgets.qml into a right-cluster slot
- verified against quickshell 0.3.0: toggles hide widgets, user widget
themes correctly, empty/broken config keeps everything on, no binding loops
Pilot only — qmldir must ship when the pilot is packaged. See DECISIONS.md.
ExecStartPost signalled Waybar via `pkill -RTMIN+11 waybar`; with no Waybar
running (e.g. a quickshell-only session) pkill exits 1, which failed the unit
and — via Restart=on-failure — spun wlsunset in a ~3s restart loop that
flickered the screen and could not be switched off (stuck "activating").
Prefix the ExecStartPost with `-` so a missing Waybar is non-fatal. The
refresh signal still fires when Waybar is present.
Add /etc/xdg-desktop-portal/niri-portals.conf with Inhibit=none. The gtk
portal reports Inhibit success even though nothing implements it under Niri,
so Firefox/Waterfox skips the native Wayland idle-inhibit. With no backend the
browser falls back to zwp_idle_inhibit, which Niri honors.
Nerd Fonts renamed "Ubuntu" -> "UbuntuSans"; Waybar's dead family name token-matched to Hack. Correct the explicit name and ship owned conf.d defaults mapping the generic families to the moonarch fonts.
- waybar/style.css: "Ubuntu Nerd Font" -> "UbuntuSans Nerd Font"
- conf.d/65-moonarch-fonts.conf: sans-serif/monospace, binding="strong" (weak prefer ranks behind the generic fallback)
- document fontconfig defaults in CLAUDE.md and DECISIONS.md
Coordinated fix with moonarch-pkgbuilds: post-install.sh enabled
wlsunset by default, while the PKGBUILD shipped a global-scope
WantedBy symlink. Together that made the toggle's user-scope disable
a no-op — filter persisted across reboots regardless of user intent.
Removing wlsunset from USER_SERVICES makes "off" the install default;
the toggle now works in user scope only, where disable can take effect.
The wheel-write-via-udev approach for charge_control_end_threshold has
been broken since 2026-04-08: the audit-remediation commit added
ACTION=="add" to the rule, but the threshold attribute doesn't exist
yet at the add event on Lenovo, so chmod fails silently and permissions
are never set. moonarch-batsaver-toggle has been returning Permission
denied since.
Replace the udev-rule approach with a pkexec helper:
defaults/bin/moonarch-batsaver-apply privileged: validate + write
defaults/bin/moonarch-batsaver-toggle user: read sysfs, dispatch via pkexec
defaults/bin/moonarch-batsaver-restore boot-time root restore (extracted
from inline ExecStart for clarity)
Default Standard-pkexec prompt — password cached per session for the
~5min auth window; no polkit no-password rule, no privilege escalation
surface from misvalidated input. Same pattern Battery-Health-Charging
GNOME extension uses.
The boot-time restore service now skips the kernel write when the
sysfs value already matches the saved state (Lenovo drivers reject
same-value writes with EINVAL).
DECISIONS.md documents the failure analysis and trade-offs.
CLAUDE.md updated to describe the new flow.
moonarch-doctor: udev-effectiveness check removed.
The `[moonarch-pkgbuilds]` paru-repo check was a false failure: that
mechanism was retired on 2026-04-20 and the install hook strips the
legacy paru.conf section on upgrade.
Audit of the rest of the doctor surfaced two related gaps — the
user-services loop skipped `walker.service` and `nautilus.service`,
even though moonarch-git ships both and enables them via
graphical-session.target.wants. Added them to the loop and filled in
the missing `wlsunset` in the CLAUDE.md listing.
Long-standing gaps in post-install.sh plus cleanup:
- post-install.sh:18 was `sudo pacman -S paru` on the wrong
assumption paru had landed in [extra]. Verified: paru/paru-bin
are AUR-only. Restored the original git-clone + makepkg
bootstrap, added the rust buildep that archinstall does not
pull in.
- post-install.sh never installed AUR extras — walker, elephant,
waypaper, stasis, themes all silently skipped. Now pulls
packages/aur.txt after moonarch-git.
- packages/official.txt: drop glab, go, npm (unused) and rustup
(only needed for the paru build, handled imperatively now).
- packages/aur.txt: add walker-bin (was missing entirely).
- transform.sh + legacy update.sh shim removed — transform was
never used in practice.
- Apollo persona block out of CLAUDE.md, prior DECISIONS entries
rewritten from Apollo/Ragnar to ClaudeCode.
- README Transform section and scripts/ listing trimmed.
- lib.sh ABOUTME updated — only post-install.sh sources it now.
Move nightlight from niri spawn-at-startup to a systemd user service
with After=kanshi.service to ensure all outputs are configured before
wlsunset starts. Toggle now uses enable/disable --now for persistent
state across reboots.
Waybar's include directive cannot merge arrays, making per-machine
module customization impossible without duplicating the entire config.
moonarch-waybar merges an optional ~/.config/waybar/userconfig with
the system config, supporting prepend/append on module arrays and
object merge for module definitions. Generates user style.css with
@import of system styles on first run.
System waybar config converted from JSONC to valid JSON for jq
compatibility. Niri startup and hotkey updated to use the wrapper.
Add blue light filter support via wlsunset, toggleable through a Waybar
module in the brightness group. Nightlight icon is the primary element,
backlight slider expands on click.
- Add wlsunset package to official.txt
- Add moonarch-nightlight toggle script (kill/restart wlsunset)
- Add moonarch-waybar-nightlight status script (JSON output)
- Add custom/nightlight module to group/brightness in Waybar config
- Add Catppuccin Yellow highlight for active nightlight state
- Add wlsunset autostart in Niri config (5000K night temperature)
Laptops with charge_control_end_threshold support get a click-to-toggle
on the battery module (80% ↔ 100%). A ♥ icon appears when conservation
is active, hidden when inactive. State persists across reboots via
systemd oneshot service. udev rule grants wheel group write access
so no sudo is needed for toggling.
Remove chown from archinstall custom-commands so the repo stays
root:root. Use sudo for git operations in update.sh. Any user with
sudo can now run moonarch-update without owning the repo.
Greeter von regreet (niri-Wrapper) auf moongreet (GTK4 + gtk4-layer-shell)
umgestellt. moongreet wird direkt aus dem Gitea-Repo gebaut.
Ein einziges Wallpaper unter /usr/share/moonarch/wallpaper.jpg dient jetzt
als Default fuer Desktop (waypaper), Lockscreen (gtklock) und Greeter
(moongreet).
Niri-basierter Wayland-Desktop mit greetd/regreet, Catppuccin Mocha
Theming, Rofi-Menus, Waybar und vollstaendiger Post-Install-Automatisierung.
Archinstall-Config klont das Repo automatisch via custom-commands,
danach genuegt ein einzelner Befehl fuer die komplette Einrichtung.