Per the committed=English rule. Also documents the windowed-video idle-inhibit added in the same series.
7.5 KiB
Moonarch
Reproducible Arch Linux setup based on archinstall + post-install automation.
Project Structure
config/— archinstall configuration (incl. custom-commands that clone the repo to /opt/moonarch, root-owned)scripts/— post-install and helper scriptspackages/— package lists (official + AUR), maintained separatelydefaults/— XDG configs, shell config, helper binaries, systemd services, udev rules, greetd/moongreet config, wallpaper
Battery Conservation Mode
Laptops with charge_control_end_threshold support (ThinkPad, Framework, etc.) get a Waybar toggle:
- Clicking the battery module toggles the charge limit between 80% and 100%
- When conservation is active, a ♥ icon appears next to the battery indicator
- State is persisted in
/var/lib/moonarch/batsaver-thresholdand restored on boot via a systemd service (moonarch-batsaver-restore) - Toggle flow:
moonarch-batsaver-toggle(user script) reads sysfs, decides 80↔100, callspkexec /usr/bin/moonarch-batsaver-apply $NEWfor the privileged sysfs+state write step. Standard pkexec prompt (password once per session cache) - On desktops without battery support the feature is hidden entirely
Nightlight (Blue Light Filter)
Waybar toggle for wlsunset (Wayland-native blue light filter), persistent state via systemd:
wlsunset.service(systemd user service) withAfter=kanshi.service— starts only once all outputs are configured- Default OFF — fresh installs start without the filter. The PKGBUILD deliberately creates NO symlink for
wlsunsetin/etc/systemd/user/graphical-session.target.wants/, and post-install.sh does not enable the service. - Clicking the nightlight module in
group/brightnesstoggles wlsunset on/off (enable --now/disable --now) - State survives reboots (user-scope symlink in
~/.config/systemd/user/...wants/) - Active state shows in Catppuccin Yellow, inactive in the default text color
- Signal SIGRTMIN+11 for immediate Waybar refresh
- Scripts:
moonarch-nightlight(toggle),moonarch-waybar-nightlight(status JSON) - Important: Never create a global-scope symlink at
/etc/systemd/user/...wants/wlsunset.service— it overrides any userdisableand makes the filter effectively impossible to turn off.
Waybar Config Merger (moonarch-waybar)
Waybar is started via moonarch-waybar (not directly). The wrapper merges an optional user config (~/.config/waybar/userconfig) with the system config (/etc/xdg/waybar/config):
prepend/appendkeys in the userconfig extend themodules-left/modules-center/modules-rightarrays- All other top-level keys are inserted as module definitions via object merge
- The merge runs only on changes (timestamp comparison)
- On error:
notify-send+logger, Waybar starts with the system config - Generates
~/.config/waybar/style.csswith an@importof the system styles if not present - Requires
jq(declared as a dependency in the PKGBUILD) - The system config must be valid JSON (no JSONC)
mpv + ModernZ OSC
The video player is mpv with ModernZ as the OSC, thumbnails via thumbfast:
mpv-modernz-gitprovidesmodernz.lua+ font + default config to/etc/mpv/mpv-thumbfast-gitprovidesthumbfast.luato/etc/mpv/scripts/(auto-detected by ModernZ)defaults/etc/mpv/mpv.confis installed directly to/etc/mpv/mpv.confby moonarch-git (owned)- Stock OSC + title bar disabled,
autofit-larger=80%x80%caps oversized windows - ModernZ overrides via
script-opts-appendin mpv.conf: orange accent → Catppuccin Lavender (#b4befe), OSC scale 0.75,window_title_font_size=18,ontop_button=no - Important: mpv treats
#as a mid-line comment; hex colors must be quoted:script-opts-append="modernz-seekbarfg_color=#b4befe"(not\#, which only escapes and swallows the rest) - Niri opens mpv floating (
window-ruleindefaults/xdg/niri/config.kdl)
System Health Check (moonarch-doctor / moondoc)
Diagnostic script that checks the system state against moonarch defaults:
- Packages (official.txt + aur.txt installed? Orphans?)
- System services (NetworkManager, bluetooth, greetd, ufw, auto-cpufreq, etc.)
- User services (kanshi, wlsunset, stasis, walker, nautilus, cliphist-text, cliphist-image)
- Config files (SHA256 comparison deployed vs. moonarch default)
- Helper scripts + symlinks (moonup, moondoc)
- System config (UFW, pacman/paru repos, default shell)
- Directories + permissions
Fontconfig Defaults
System-wide generic-family defaults via defaults/etc/fonts/conf.d/65-moonarch-fonts.conf (owned by moonarch-git):
sans-serif→ UbuntuSans Nerd Font,monospace→ UbuntuSansMono Nerd Font- Number 65: loads after
60-latin.conf, so the moonarch prefs beat the stock default (Noto/DejaVu).local.conf(at 51 via51-local.conf) loads too early and is overridden by 60-latin — therefore do not use it (it is also reserved for local user overrides). - Aliases need
binding="strong"— a weak<prefer>(fontconfig default) ranks behind the effective generic fallback and does not take effect. - Only applies to apps that use generic families (e.g. Firefox web fallback). moonarch apps (Waybar, foot, GTK, walker, swaync) set the font explicitly.
Browser Idle-Inhibit (xdg-desktop-portal)
So that windowed browser video (Firefox/Waterfox) keeps the screen awake, via defaults/etc/xdg-desktop-portal/niri-portals.conf (owned by moonarch-git, higher priority than niri's /usr/share/xdg-desktop-portal/niri-portals.conf):
xdg-desktop-portal-gtkreports theInhibitinterface as success even though nobody implements it under Niri → Firefox believes the idle-inhibit went through the portal and does not use the native Waylandidle-inhibit. Result: no inhibitor, screen sleeps.- Fix:
org.freedesktop.impl.portal.Inhibit=none→ Firefox falls back tozwp_idle_inhibit, which Niri honors. The remaining[preferred]lines are taken 1:1 from niri's default (portals.conf is not merged — the highest-priority file applies in full). - stasis is uninvolved here:
monitor_media(pactl) does not capture browser audio by design (non-browser players only); browsers go through the inhibit path. - Activation: restart xdg-desktop-portal + the browser — Firefox queries portal support at startup.
Windowed / Muted Video: wayland-pipewire-idle-inhibit
The portal fix only applies to fullscreen video — Firefox/Waterfox send the idle-inhibit only in fullscreen. Windowed video remains unprotected: stasis ignores browser audio (pactl, browser-excluded, no MPRIS backend), Niri gets no inhibitor, the screen sleeps mid-video.
Solution: wayland-pipewire-idle-inhibit (AUR, systemd user service; in packages/aur.txt + post-install.sh USER_SERVICES + moonarch-doctor). Holds a Wayland zwp_idle_inhibit inhibitor while PipeWire outputs audio (default threshold 5s). Niri then suppresses idle → stasis stays awake, daemon-independent. Releases automatically when audio stops.
- Catches windowed video with sound. Misses muted video (no audio signal) — edge case, accepted.
- Never put
waterfox/a browser in stasisinhibit_apps: that inhibits unconditionally as long as the browser process runs → the system never idles. This exact wrong fix caused the "no idle" bug.
Conventions
- Package lists are plain text files, one package per line, comments with
# - Shell scripts must be POSIX-compatible or explicitly bash/zsh
- All paths in the archinstall config are relative to the install target