Caelestia (Hyprland Desktop) — From Bare Arch to a Working Bar
The Caelestia desktop layer — one of two desktop options for the install in Article 03 (Arch + LUKS + Btrfs). Split into its own article because the desktop is independent of the underlying install — you can follow this on top of any working Arch base (LUKS+Btrfs, plain ext4, dual-boot, whatever). The commands below all run inside a booted, networked Arch system with a regular user account.
In order:
- PipeWire audio stack (so sound works the moment Caelestia launches)
- System prerequisites Caelestia expects to find
- LazyVim on top of the Neovim that’s already in
base - Graphics drivers for Intel / AMD / NVIDIA / hybrid laptops
- An AUR helper (
yay-bin) - Caelestia itself —
caelestia-meta+ the install script - GUI applications before the first reboot (Firefox, Nautilus, LibreOffice, KDE apps)
- SDDM as the login manager
- The first reboot into the desktop
- Post-login verification — wallpaper chain, Hyprland customization paths, sanity checks
If your install isn’t done yet, finish the bootstrap-and-LUKS-and-Btrfs half first (Article 03 Parts 1–3) and come back here.
If you want a custom-built Hyprland setup instead (no project-bundle layer, your own bar / terminal / theme picks), see the read-this-first callout below — the upcoming Custom Hyprland article is for you.
This article is honest about what I shipped and what I learned. Read this before committing to the bundle.
- On older hardware it ran slow. The Quickshell-rendered bar + Material-You wallpaper-driven retheming + animated reveals add real frame-time pressure. On my old test laptop the desktop was usable but noticeably less snappy than a barebones Hyprland with
waybar. On modern hardware (the Precision the rest of the article is tested on) it’s fine. - The install was harder than many tutorials made it look. The
caelestia-meta.SRCINFOparsing failure I document below is real, and several other small papercuts (the missinghypr-vars.confsource =line that paints a red overlay on first login, the wallpaper-daemon-not-installed-by-default chain, the SSH-vs-local-seat trap withhyprctl) cost me hours that none of the upstream guides flagged. If you’re impatient or new to Hyprland, expect troubleshooting time before “first screenshot.” - It’s opinionated past my taste. The bundle picks every layer for you — bar, terminal (
foot), shell (fish+starship), keybinds, animations, file-manager (nautilus-friendly), theme. That’s the point of a bundle, and a lot of people love it. For me, after living in it, I’d rather build a Hyprland setup piece by piece so I own every keybind and every line ofhyprland.conf/hyprland.lua. - What I’m doing next: my next install will skip Caelestia and follow the upcoming Custom Hyprland article —
waybarorironbarinstead of Quickshell, my own keybind set, a much smaller dependency footprint, no AUR meta-package gymnastics, and (importantly) no bundled retheming layer. This Caelestia article stays published as honest documentation of what it took to ship the opinionated path, for readers who do want the turnkey route.
None of the above is a recommendation against Caelestia — it’s a project I respect and the screenshots are deserved. It’s a fit assessment. If you want a polished Hyprland desktop in an evening and don’t mind inheriting opinions, go for it. If you’d rather take longer and own every piece, the Custom Hyprland article is the one to wait for.
Part 4 — Caelestia (Hyprland desktop)
Now we layer the desktop. Caelestia is a Hyprland-based dotfile bundle with a curated set of tools (foot, fish, fastfetch, starship, btop, eza, …) and a Quickshell-based status bar. The project ships both an AUR meta-package (caelestia-meta) and an install.fish script that symlinks configs into ~/.config.
1. Audio stack (PipeWire)
caelestia-meta depends on wireplumber (the session manager), but it does not pull the PipeWire backend itself or the drop-in shims that replace PulseAudio / ALSA / JACK clients. Install the whole stack explicitly so the desktop has working sound from the first launch:
sudo pacman -S \
pipewire pipewire-audio wireplumber \
pipewire-pulse pipewire-alsa pipewire-jack \
pavucontrol \
sof-firmwareWhen pacman prompts about a JACK provider, choose pipewire-jack so the PipeWire shim wins. When it prompts about a PulseAudio replacement, choose pipewire-pulse for the same reason.
Click to expand: what each package does
| Package | Role |
|---|---|
pipewire |
The PipeWire server itself — handles audio (and video) graph routing. |
pipewire-audio |
Audio subset of PipeWire’s daemon configs and helpers. |
wireplumber |
Session/policy manager on top of PipeWire (device probing, default routing). |
pipewire-pulse |
PulseAudio-compatible socket — apps written for PulseAudio talk to PipeWire transparently. |
pipewire-alsa |
ALSA-to-PipeWire shim — apps using raw ALSA route through PipeWire. |
pipewire-jack |
JACK-to-PipeWire shim — pro-audio apps that expect JACK get a working JACK. |
pavucontrol |
The classic PulseAudio volume / routing GUI. Works fine against pipewire-pulse and is still the easiest way to pin specific apps to specific outputs. |
sof-firmware |
Sound Open Firmware blobs — required for most modern Intel laptop audio codecs. |
The PipeWire and WirePlumber user services start automatically once a graphical session is active (SDDM → Hyprland triggers graphical-session.target for your user, which fans out and starts the socket-activated PipeWire units). On a bare TTY before that happens, they’re idle and pactl / wpctl will return Connection refused — that’s not a broken install, just “no graphical session has woken them yet.”
To verify the install now, before the desktop is up, kick the user services manually one time:
systemctl --user enable --now pipewire.socket pipewire-pulse.socket wireplumber.serviceThen run the verification commands:
pactl info | grep 'Server Name' # should print: PulseAudio (on PipeWire 1.6.x)
wpctl status # should print a tree of devices, sinks, and sourcesIf pactl info reports PulseAudio (on PipeWire …), the stack is wired correctly and pipewire-pulse is intercepting the PulseAudio API as intended. If wpctl status shows your audio devices (built-in speakers, headphone jack, HDMI sinks), WirePlumber’s device probing is alive.
| Symptom | What’s wrong | Fix |
|---|---|---|
pactl info reports actual PulseAudio (no “on PipeWire” suffix) |
Something pulled pulseaudio in as a dependency conflict and the real PulseAudio daemon won |
pacman -Qs pulseaudio to find the offending package, uninstall it, then reinstall pipewire-pulse |
Connection refused from pactl / wpctl on a bare TTY |
User services aren’t running yet — no graphical session has triggered them | Run the systemctl --user enable --now line above. After your next graphical login, they auto-start and you’ll never need to do this by hand again. |
Connection refused even after systemctl --user start |
XDG_RUNTIME_DIR isn’t set, or the user manager isn’t running |
loginctl show-user $USER -p Linger — if Linger=no and you SSH’d in, log out and log in directly at the TTY instead, or sudo loginctl enable-linger $USER |
2. System prerequisites Caelestia expects to find
caelestia-meta lists a lot of dependencies but assumes a few CLI essentials are already on the system. Install these now so the install script and theme compilation both have everything they need on first run:
sudo pacman -S --needed \
terminus-font \
curl wget \
perl \
gcc make cmake \
sassc \
fish \
bluez bluez-utilsClick to expand: what each package does
| Package | Why we want it before Caelestia |
|---|---|
terminus-font |
Crisp bitmap console font (ter-132b, ter-116n, …). Useful inside a TTY when X / Wayland is broken — i.e., exactly the situation you’ll be in if a Caelestia update misbehaves. |
curl, wget |
Half of every shell script on the internet calls one of these. Neither is in base. |
perl |
Pulled in by some build tools and xdg- utilities; safer to have it explicitly than discover it missing inside a failing build. |
gcc, make, cmake |
gcc and make come with base-devel (already installed in the chroot), but cmake doesn’t. Required for any package — Quickshell included — that builds with CMake. |
sassc |
Sass compiler. Used by the GTK theme and several status-bar widgets Caelestia bundles. Missing this is a silent “no styles loaded” failure on first launch. |
fish |
The Caelestia install script (install.fish) is fish-only. caelestia-meta brings fish in too, but pre-installing it means the install script can run before the meta-package has even finished depsolving. |
bluez |
The Linux Bluetooth stack (kernel-side helpers, bluetoothd). Required for any Bluetooth headset, mouse, or keyboard. |
bluez-utils |
Userspace utilities — bluetoothctl, hciconfig, the test scripts. Without it you’d have a running daemon you can’t talk to. |
Enable the Bluetooth daemon so it starts on every boot:
sudo systemctl enable --now bluetooth.serviceThe --needed flag tells pacman to skip anything already present (some of these are pulled in by base-devel), so this command is safe to re-run any time.
3. LazyVim on top of Neovim
Neovim is in base and was installed by pacstrap. LazyVim layers a curated plugin set, sensible defaults, and a per-language LSP setup on top of it. The official install path is to drop the LazyVim starter config into ~/.config/nvim:
# Back up any existing config (paranoid but cheap)
mv ~/.config/nvim ~/.config/nvim.bak 2>/dev/null
mv ~/.local/share/nvim ~/.local/share/nvim.bak 2>/dev/null
mv ~/.local/state/nvim ~/.local/state/nvim.bak 2>/dev/null
mv ~/.cache/nvim ~/.cache/nvim.bak 2>/dev/null
# Clone the starter
git clone https://github.com/LazyVim/starter ~/.config/nvim
rm -rf ~/.config/nvim/.git # so it becomes *your* config repo, not theirs
# First launch — plugins install themselves on the first time you open nvim
nvimThe first nvim launch will pop up a Lazy.nvim progress window while the plugins clone and compile. Wait for :Lazy to report all plugins installed, then :q. If you prefer the AUR-packaged version instead, yay -S lazyvim after installing the AUR helper below also works — same end state.
4. Graphics drivers
First figure out which GPU(s) you actually have — don’t guess, the wrong driver set wastes a few hundred MB of install and either does nothing useful or causes silent breakage at first compositor launch.
lspci -nn | grep -E 'VGA|3D|Display'Interpret the output by vendor string:
Click to expand
What lspci reports |
GPU you have | Run block(s) below |
|---|---|---|
Intel Corporation … (Graphics\|Iris\|UHD\|Arc) … |
Intel iGPU (or Arc dGPU) | Intel |
Advanced Micro Devices, Inc. [AMD/ATI] … |
AMD GPU (APU iGPU or Radeon dGPU) | AMD |
NVIDIA Corporation … |
NVIDIA dGPU | NVIDIA |
| One Intel line and one NVIDIA line | Hybrid laptop (Intel iGPU + NVIDIA dGPU) | Both Intel and NVIDIA |
| One AMD line and one NVIDIA line | Hybrid (AMD APU + NVIDIA dGPU) | Both AMD and NVIDIA |
| Two AMD lines | AMD APU + AMD dGPU | AMD (covers both) |
On a hybrid laptop, install both blocks — the drivers coexist, Hyprland defaults to the iGPU for everyday work to save power, and apps that need the dGPU request it explicitly.
Machine examples
A clean run on an unknown machine should look like this — one or two lspci lines, no errors, then the install of the matching driver block(s). The specific package names and DKMS output below are templates; substitute your kernel version and driver version.
$ lspci -nn | grep -E 'VGA|3D|Display'
# one or two lines, depending on whether you have a hybrid GPU laptopThen run the matching driver block from the lists below, verify with vainfo (Intel/AMD VA-API) and/or nvidia-smi (NVIDIA), and reboot if you installed the NVIDIA driver for the first time.
The mobile workstation I’ve fully installed and tested this guide on. Alder Lake-P + GA107GLM (Ampere) — a hybrid laptop, so both driver blocks were needed.
lspci -nn | grep -E 'VGA|3D|Display':
0000:00:02.0 VGA compatible controller [0300]: Intel Corporation Alder Lake-P GT2 [Iris Xe Graphics] [8086:46a6] (rev 0c)
0000:01:00.0 3D controller [0302]: NVIDIA Corporation GA107GLM [RTX A2000 8GB Laptop GPU] [10de:25ba] (rev a1)
Two GPU lines → install both blocks. Intel first:
sudo pacman -S mesa vulkan-intel intel-media-driver libva-utilsVerify Intel side with vainfo. On the Precision you should see the full Iris Xe codec set including hardware AV1 decode (VAProfileAV1Profile0 : VAEntrypointVLD) — confirms intel-media-driver 26.1.5 is loaded:
$ vainfo
Trying display: drm
vainfo: VA-API version: 1.23 (libva 2.22.0)
vainfo: Driver version: Intel iHD driver for Intel(R) Gen Graphics - 26.1.5 ()
vainfo: Supported profile and entrypoints
VAProfileNone : VAEntrypointVideoProc
VAProfileH264High : VAEntrypointVLD
VAProfileHEVCMain10 : VAEntrypointVLD
VAProfileVP9Profile2 : VAEntrypointVLD
VAProfileAV1Profile0 : VAEntrypointVLD # <-- Iris Xe HW AV1 decode
...
Then NVIDIA:
sudo pacman -S nvidia-open-dkms nvidia-utils libva-nvidia-driverThe DKMS hook compiles nvidia/595.71.05 -k 7.0.7-arch2-1 (driver version × kernel version) and rebuilds the initramfs to include nvidia early. You’ll see snap-pac take pre/post snapshots bracketing the transaction.
nvidia-smi will fail until you reboot
Immediately after pacman -S nvidia-open-dkms, running nvidia-smi returns:
NVIDIA-SMI has failed because it couldn't communicate with the NVIDIA driver.
Make sure that the latest NVIDIA driver is installed and running.
And sudo modprobe nvidia returns:
modprobe: ERROR: could not insert 'nvidia': No such device
This is expected and not a broken install. The running kernel was booted before nvidia-open-dkms existed, so the nouveau open-source NVIDIA driver is still bound to the dGPU. nouveau owns the device; nvidia can’t bind on top. The package install dropped a nouveau blacklist file and rebuilt your initramfs to load nvidia early on next boot — both take effect only after a reboot.
Fix: sudo reboot. After re-entering your LUKS passphrase and logging back in:
$ lsmod | grep nvidia
# expected: nvidia, nvidia_drm, nvidia_modeset, nvidia_uvm
$ nvidia-smi
# expected: table showing "NVIDIA RTX A2000 8GB Laptop GPU", driver 595.71.05, ~5-10 W idleIf nvidia-smi still fails post-reboot, add nvidia_drm.modeset=1 to GRUB_CMDLINE_LINUX_DEFAULT in /etc/default/grub, re-run grub-mkconfig -o /boot/grub/grub.cfg, and reboot again. On nvidia-open 595+ this parameter is usually defaulted, but some hybrid configurations need it explicit.
Success state on the Dell Precision after the reboot
What you should see if everything came up cleanly:
$ lsmod | grep nvidia
nvidia_drm 151552 0
nvidia_modeset 2195456 1 nvidia_drm
nvidia_uvm 2490368 0
nvidia 16568320 2 nvidia_uvm,nvidia_modeset
drm_ttm_helper 20480 2 nvidia_drm,xe
video 81920 5 dell_wmi,dell_laptop,xe,i915,nvidia_modeset
All four NVIDIA modules loaded — nvidia, nvidia_drm, nvidia_modeset, nvidia_uvm. Notice that video shows both i915 and xe loaded for the Intel side: xe is the modern Mesa-friendly Intel driver, i915 is the legacy one — current kernels load both and let the device pick. Alder Lake-P GT2 binds i915; xe is loaded as available. dell_wmi and dell_laptop are the Dell ACPI bits (lid switch, brightness, backlight) wired into the video subsystem.
$ nvidia-smi
Sun May 17 22:44:06 2026
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 595.71.05 Driver Version: 595.71.05 CUDA Version: 13.2 |
+-----------------------------------------+------------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
|=========================================+========================+======================|
| 0 NVIDIA RTX A2000 8GB Lap... Off | 00000000:01:00.0 Off | N/A |
| N/A 55C P0 8W / 35W | 0MiB / 8192MiB | 0% Default |
+-----------------------------------------+------------------------+----------------------+
| Processes: No running processes found |
Key things to read out of that table:
Click to expand: what each field means
| Field | Value | What it tells you |
|---|---|---|
| Driver Version | 595.71.05 |
nvidia-open from Arch’s nvidia-open-dkms package |
| CUDA Version | 13.2 |
The CUDA version the driver supports — install pacman -S cuda later if you want to do ML/PyTorch work, it’ll talk to this driver |
| Persistence-M | Off |
Power management lets the GPU sleep between accesses (correct for a laptop; saves battery) |
| Temp | 55°C |
Idle temp after nvidia-smi briefly woke it; will drop in the next few seconds |
| Pwr Usage / Cap | 8 W / 35 W |
Wake-up draw; the long-term idle floor is ~0.5 W when D3cold-suspended |
| Memory-Usage | 0 MiB / 8192 MiB |
All 8 GB of VRAM available |
| GPU-Util | 0 % |
Nothing using the GPU right now (no processes) |
nvidia-open driver versions 525+ enable D3cold runtime power management automatically for capable Turing-or-newer GPUs. The RTX A2000 is one of them, so on a current Arch install the dGPU should be runtime-suspending without any kernel-cmdline work from you. Confirm with:
cat /proc/driver/nvidia/gpus/0000:01:00.0/powerOn the Precision, this prints:
Runtime D3 status: Enabled (fine-grained)
Tegra iGPU Rail-Gating: Disabled
Video Memory: Off
GPU Hardware Support:
Video Memory Self Refresh: Supported
Video Memory Off: Supported
S0ix Power Management:
Platform Support: Supported
Status: Disabled
Notebook Dynamic Boost: Supported
The two lines that matter:
Runtime D3 status: Enabled (fine-grained)— runtime PM is on, and “fine-grained” means the driver can suspend individual function blocks independently (best-case power management).Video Memory: Off— the 8 GB of GDDR6 is fully powered down right now, dropping idle draw to ~0.5 W instead of the ~8 W you see whilenvidia-smiis querying.
S0ix Power Management … Status: Disabled is not a problem — S0ix is the system-wide modern-standby state, and Disabled here just means your laptop isn’t currently suspended. It’ll activate the next time you systemctl suspend.
Only if Runtime D3 status: Disabled (older driver, or some hybrid configs where auto-enable doesn’t trigger) do you need to opt in manually — add nvidia.NVreg_DynamicPowerManagement=0x02 to GRUB_CMDLINE_LINUX_DEFAULT and re-run sudo grub-mkconfig -o /boot/grub/grub.cfg. On nvidia-open 595+ with an Ampere or newer card, this is unnecessary.
After the reboot, the four Hyprland NVIDIA env vars (set later, in ~/.config/hypr/hyprland.conf) become relevant — see the env-vars table above.
Placeholder. I haven’t installed this stack on the Dell Pro 16 Plus yet. When I do, this tab will land with: the real lspci output (likely Intel Lunar Lake or Arrow Lake iGPU only, no dGPU on the Pro line), the matching pacman -S block, vainfo confirmation, and any laptop-specific quirks I hit (firmware updates, ACPI workarounds, audio codec patches).
Intel iGPU (Iris, UHD, Arc):
sudo pacman -S mesa vulkan-intel intel-media-driver libva-utils| Package | Role |
|---|---|
mesa |
The userspace OpenGL / Vulkan implementation for Intel, AMD, and most open-source GPU drivers. Required for any 3D acceleration under Wayland. |
vulkan-intel |
Vulkan ICD (Installable Client Driver) for Intel GPUs. Needed so apps using Vulkan see the iGPU. |
intel-media-driver |
VA-API hardware video decode for Intel Broadwell and newer. Lets browsers and mpv decode H.264/HEVC/AV1 on the GPU instead of the CPU. |
libva-utils |
vainfo and friends — diagnostic CLIs for verifying VA-API works after install. |
AMD:
sudo pacman -S mesa vulkan-radeon libva-mesa-driver mesa-vdpau libva-utilsClick to expand: what each package does
| Package | Role |
|---|---|
mesa |
OpenGL / Vulkan userspace (same as Intel). |
vulkan-radeon |
Vulkan ICD for AMD GPUs (RADV driver). |
libva-mesa-driver |
VA-API hardware video decode for AMD via Mesa. |
mesa-vdpau |
VDPAU hardware video decode for AMD (older API some apps still prefer). |
libva-utils |
vainfo / diagnostics. |
NVIDIA on Wayland — the open kernel modules are the recommended path in 2026:
sudo pacman -S nvidia-open-dkms nvidia-utils libva-nvidia-driver| Package | Role |
|---|---|
nvidia-open-dkms |
NVIDIA’s open-source kernel modules, built against your current kernel via DKMS. Required for Turing (GTX 16xx / RTX 20xx) or newer. |
nvidia-utils |
Userspace libraries — libGL, CUDA stub, nvidia-smi. |
libva-nvidia-driver |
VA-API shim that routes hardware decode through NVDEC. |
Verify after install:
vainfoYou should see a list of supported codecs without errors. If you switched the kernel (added linux-lts etc.), re-run mkinitcpio -P so DKMS rebuilds NVIDIA against every installed kernel.
These are libraries, not services — there’s no systemctl enable nvidia to forget. The GPU comes up the moment the kernel loads its driver, well before login.
If your lspci showed two GPU lines — typical on mobile workstations (Dell Precision, ThinkPad P-series, HP ZBook) and most gaming laptops — install the driver blocks for both vendors. The drivers coexist without conflict; the kernel loads each as its hardware appears, and Wayland compositors pick which one to render on a per-app basis.
The default behavior under Hyprland on a hybrid:
- The iGPU (Intel Iris Xe, AMD Radeon Graphics) is the primary render device — it draws your compositor, your windows, your animations, video playback, and almost every app. It idles at low power and keeps your battery happy.
- The dGPU (NVIDIA RTX, AMD Radeon dGPU) idles at ~5 W until explicitly asked to render. Apps that want it call
prime-run <app>or set__NV_PRIME_RENDER_OFFLOAD=1themselves (CUDA jobs, ML training, Blender, games, video encoders).
A few things to expect during the NVIDIA install on a hybrid:
- Pacman prompts to pick a
libglvndprovider — picknvidia-utils.mesaandnvidia-utilsboth provide pieces of libGL; the GLVND dispatcher at runtime routes calls to whichever GPU each app is actually rendering on. - DKMS compiles the
nvidiakernel modules against your current kernel (~30–60 seconds, runs inside pacman’s post-install hook).linux-headersfrom pacstrap is what makes this work — without it, the compile fails loudly. The-open-dkmsvariant builds against NVIDIA’s open kernel modules, which is the recommended path for any GPU Turing or newer (GTX 16-series, RTX 20-series, all RTX 30-series, all RTX 40/50-series, all RTX A/L workstation cards). - A
nvidia.confmodules-load file drops into/etc/modules-load.d/so the modules load on next boot, before the display server starts.
Verify both halves of the GPU stack after install:
vainfo # Intel iGPU: lists h264, h265, vp9, av1 decode entrypoints
nvidia-smi # NVIDIA dGPU: prints a table showing the dGPU name,
# driver version, and current power draw (~5–10 W idle)If nvidia-smi returns “No devices were found” or hangs, the kernel modules didn’t load — usually a silent DKMS build failure. Recovery: sudo dkms autoinstall && sudo modprobe nvidia, then re-check.
Hyprland env vars for NVIDIA (set once Caelestia is up)
Don’t worry about these yet — flagging so you know to look for them after Caelestia is installed and Hyprland is running. NVIDIA on Wayland needs a handful of environment variables to interoperate cleanly with Hyprland and the apps that run inside it. Caelestia’s bundled hyprland.conf may already set them; if not, add them to ~/.config/hypr/hyprland.conf (Hyprland’s syntax is env = NAME,VALUE — comma, not equals):
env = LIBVA_DRIVER_NAME,nvidia
env = __GLX_VENDOR_LIBRARY_NAME,nvidia
env = NVD_BACKEND,direct
env = ELECTRON_OZONE_PLATFORM_HINT,auto
What each variable does:
| Variable | What it does | When to set it |
|---|---|---|
LIBVA_DRIVER_NAME=nvidia |
VA-API (Video Acceleration API) is the Linux API for hardware-accelerated video decode/encode. This env var tells clients (browsers, mpv, obs) which VA-API driver to load. nvidia routes decode to the NVIDIA GPU via the libva-nvidia-driver shim. |
Set if you want the dGPU doing video decode (faster on 4K, more CPU-cool). Omit on battery-sensitive workflows — leaving it unset (or setting iHD for Intel) keeps decode on the iGPU, which is more power-efficient. |
__GLX_VENDOR_LIBRARY_NAME=nvidia |
GLVND (GL Vendor-Neutral Dispatch) is the library that lets multiple GPU drivers coexist on one system. This env var tells GLVND to route OpenGL/GLX calls through the NVIDIA libGL implementation. | Set when you want OpenGL apps (legacy X11 apps running via Xwayland) to use the NVIDIA dGPU instead of the Intel iGPU. Less important for native Wayland apps, which use EGL via the compositor’s chosen device. |
NVD_BACKEND=direct |
Tells the libva-nvidia-driver shim to talk to NVDEC (NVIDIA’s hardware video decoder) via the direct backend, rather than going through CUDA-based intermediaries. The direct backend is lower-latency and the recommended path for current driver versions. |
Always set this when LIBVA_DRIVER_NAME=nvidia is also set. The two work together — the first picks NVIDIA for VA-API, the second tells NVIDIA how to do the decode. |
ELECTRON_OZONE_PLATFORM_HINT=auto |
Chromium and Electron apps (VSCode, Discord, Slack, Obsidian, Signal, Spotify, Zen Browser) use the “Ozone” rendering abstraction. auto tells them to detect Wayland and prefer the native Wayland backend over Xwayland. |
Always set this on Hyprland regardless of GPU — not NVIDIA-specific. Without it, Electron apps run via Xwayland (works, but ignores fractional scaling, has worse touchpad gestures, and is slightly laggier on hi-DPI displays). |
A few additional notes:
Apply after adding — Hyprland reloads its config on save for most settings, but
envlines only take effect for apps started after the change. Either log out and log back in, orhyprctl reloadand then close+reopen each app you want to pick up the new env.For the iGPU-only render path (your default for desktop work), no env vars are needed at all — Hyprland uses the iGPU automatically. The vars above only matter when you want NVIDIA to do specific work.
For running a specific app on the dGPU, you don’t have to flip any global env vars — just launch it with offload:
prime-run blender # or: __NV_PRIME_RENDER_OFFLOAD=1 __GLX_VENDOR_LIBRARY_NAME=nvidia blenderprime-runis a tiny wrapper script that comes withnvidia-prime(worthpacman -S nvidia-primeif you do a lot of per-app dGPU offload).Older NVIDIA driver workarounds you might see online — older guides recommend
WLR_NO_HARDWARE_CURSORS=1(fixes invisible cursor on pre-555 drivers) andGBM_BACKEND=nvidia-drm(sets the GBM allocator). Withnvidia-open-dkms≥ 580 (current as of mid-2026) neither is needed — skip them unless you actually see a problem they describe.
5. Install an AUR helper
Caelestia is distributed via the AUR and its install script defaults to yay:
sudo pacman -S --needed git base-devel
git clone https://aur.archlinux.org/yay-bin.git /tmp/yay-bin
cd /tmp/yay-bin
makepkg -si
cd ~| Package | Role |
|---|---|
git |
The version-control client. Needed to clone the AUR PKGBUILD. Already installed in the chroot but listed with --needed so the line is self-contained. |
base-devel |
Toolchain meta-package (gcc, make, pkgconf, fakeroot, binutils, …). Required for any PKGBUILD to compile. Also already in the chroot. |
yay-bin |
The AUR helper itself, distributed as a pre-built binary so the bootstrap doesn’t need to compile Go just to install yay. After this, yay <pkg> works like pacman -S <pkg> but also searches the AUR. |
No service to enable — yay is just a CLI tool.
6. Install Caelestia
The README pattern is one command per step:
# Pull in all the dependencies (Hyprland, Quickshell, foot, fish, fastfetch, etc.)
yay -S caelestia-meta
# Clone the dotfiles into the canonical location
git clone https://github.com/caelestia-dots/caelestia.git ~/.local/share/caelestia
# Run the installer (it requires fish, which caelestia-meta just installed)
~/.local/share/caelestia/install.fish --aur-helper=yayinstall.fish symlinks every config from ~/.local/share/caelestia/ into ~/.config/. If you later move or delete the cloned repo, every symlink becomes a broken pointer and your desktop boots into a half-themed Hyprland with no bar. Treat ~/.local/share/caelestia/ as part of the install, not as scratch space. To update Caelestia later: cd ~/.local/share/caelestia && git pull (no need to re-run install.fish unless dependencies change).
caelestia-meta .SRCINFO parsing aborts the install
A reproducible failure mode I hit on a fresh install — and the fix that worked. The first time install.fish runs, you may see all 9 AUR packages build successfully (==> Validating source files with sha256sums... Passed for each), then the install phase aborts with:
:: (1/9) Parsing SRCINFO: caelestia-meta
-> failed to parse caelestia-meta: Unable to read file: .SRCINFO: open .SRCINFO: no such file or directory
:: Installing hypr* configs...
fish: Unknown command: hyprctl
~/.local/share/caelestia/install.fish (line 169):
hyprctl reload
^~~~~~^
fish: Unknown command: caelestia
~/.local/share/caelestia/install.fish (line 303):
caelestia scheme set -n shadotheme
What happened: caelestia-meta is a pure meta-package (no source code, just dependency declarations). On some yay versions and some AUR mirror states, the .SRCINFO file gets missed during the build → install handoff, and yay aborts the install phase. install.fish doesn’t check yay’s exit status — it cheerfully continues into the config-symlinking step and tries to call hyprctl and caelestia, neither of which exist on disk because the install never happened. The “Unknown command: hyprctl / Unknown command: caelestia” errors are downstream symptoms; the real failure is the SRCINFO line above them.
Recovery in four steps:
Confirm what’s actually installed — most likely nothing from the Caelestia stack made it past the parse failure:
pacman -Qs hyprland foot fish fastfetch quickshell caelestiaInstall the official-repo deps directly with pacman (skip the broken meta-package’s dependency-resolution path):
sudo pacman -S --needed \ hyprland foot fish fastfetch starship btop eza fuzzel jq \ xdg-desktop-portal-hyprland hyprpicker wl-clipboard cliphist \ trash-cli adw-gtk-theme papirus-icon-theme \ ttf-jetbrains-mono-nerd ttf-cascadia-code-nerd ttf-material-symbols-variable \ inotify-tools ddcutil brightnessctl power-profiles-daemon \ dart-sass swappy grim slurp sndio gpu-screen-recorder \ libnotify python-pillow uwsmInstall the AUR components individually, omitting
caelestia-meta— yay caches the source tarballs even when it cleans the built.pkg.tar.zstartifacts, so the rebuild is fast (Quickshell’s ~1–3 min CMake compile is the only meaningful wait):yay -S --needed \ caelestia-shell quickshell-git libcava caelestia-cli \ app2unit python-materialyoucolor ttf-rubik-vf qtengineFor the yay prompts:
Nto cleanBuild (cache is good),Nto diffs,Nto PKGBUILDs to edit,Nto remove make deps after install,Yto “Proceed with installation?”. (See the pacman + yay reference article for full prompt explanations.)Re-run install.fish so it finishes the config-symlinking step now that
hyprctlandcaelestiaexist:~/.local/share/caelestia/install.fish --aur-helper=yayAt the prompts: pick
1for “backup already exists” (your~/.configwas backed up on the first attempt), andYto every “Overwrite?” question (these are stale partial symlinks from the first run; let install.fish refresh them).
Verify the recovery:
which hyprctl caelestia quickshell foot fish # all 5 should print /usr/bin/<name>
hyprctl version # prints "HYPRLAND_INSTANCE_SIGNATURE not set!" — that's EXPECTED on a TTY/SSH session (you're outside Hyprland; the binary works, it just has nothing to talk to until a session is running)
caelestia --help # prints subcommand list
ls -l ~/.config/hypr/ # should show symlinks into ~/.local/share/caelestia/If all four commands return sane output, you’re recovered. The “HYPRLAND_INSTANCE_SIGNATURE not set!” message is not an error — it’s hyprctl correctly reporting that there’s no running Hyprland for it to talk to from your current shell. You’ll see a real version string once you’re inside a Hyprland session (after the SDDM reboot in Section 9).
Why skip caelestia-meta going forward? It’s a convenience meta-package — its only role is to declare dependencies. With the 8 component AUR packages + the pacman-installed repo deps, you have functionally the same installed state. You can retry yay -S caelestia-meta later if you want it formally recorded; if it still fails with the SRCINFO error, the practical impact is zero (you just yay -Syu <component> to update each piece individually instead of yay -Syu caelestia-meta to update the whole bundle).
Optional flags to pass to install.fish:
Click to expand: what each flag does
| Flag | What it adds |
|---|---|
--spotify |
Installs Spotify + the Spicetify theme matching Caelestia. |
--vscode=code or =codium |
Installs VS Code (or Codium) with the Caelestia VSIX theme. |
--discord |
OpenAsar Discord + the Equicord client mod with Caelestia theme. |
--zen |
Installs the Zen Browser with the Caelestia native-app manifest. |
--noconfirm |
Pass through to pacman/yay. Use only if you’ve read what’s being installed. |
7. Install applications (pre-reboot)
Now — before the login manager and final reboot — install the GUI applications you actually want on the system. Doing this now means the very first graphical login already has a working browser, file manager, office suite, and image/video tools.
sudo pacman -S \
firefox \
nautilus \
libreoffice-fresh \
gwenview kate ark okular \
haruna swayimg \
flatpak \
desktop-file-utils \
discoverdiscover is in the official repos now
Older guides (and an earlier draft of this article) reach for yay -S discover because Discover used to be AUR-only on non-Plasma desktops. As of recent Arch, it’s back in the official extra repo — sudo pacman -S discover works directly, no AUR build needed. If you do run yay -S discover you’ll see the output line Sync Explicit (1): discover-6.6.5-1 (note: Sync, not AUR) — yay just forwards to pacman.
This pacman command triggers several :: There are N providers available for X: prompts because some of these apps depend on virtual package names that multiple real packages can satisfy. Picking the wrong one isn’t usually fatal but means extra dependencies you don’t need (or worse codec coverage in your media apps). Here’s what each likely prompt will look like and what I recommend:
Click to expand: what each prompt means
| Prompt | Options | Pick | Why |
|---|---|---|---|
:: There are N providers available for ttf-font: |
gnu-free-fonts / noto-fonts / ttf-bitstream-vera / ttf-croscore / ttf-dejavu / ttf-droid / ttf-ibm-plex / ttf-input / ttf-input-nerd / ttf-liberation / ttf-roboto | noto-fonts |
Google’s Unicode-spanning family — broadest script coverage (Latin, Cyrillic, CJK fallbacks, Devanagari, Arabic, …), the de-facto modern Linux desktop default. Complements the JetBrains Mono + Material Symbols fonts Caelestia already pulled in. |
:: There are 2 providers available for qt6-multimedia-backend: |
qt6-multimedia-ffmpeg / qt6-multimedia-gstreamer | qt6-multimedia-ffmpeg |
Qt 6.7+’s own recommended default. Broader codec coverage via ffmpeg, fewer transitive deps, less GNOME-stack interop you don’t need on Hyprland. |
:: There are 2 providers available for phonon-qt6-backend: |
phonon-qt6-vlc / phonon-qt6-gstreamer | phonon-qt6-vlc |
Used by some KDE apps (Okular’s PDF annotations, parts of KDE Frameworks). VLC backend has wider codec support than GStreamer for the same reason as above. |
:: There are N providers available for java-runtime (only if libreoffice prompts) |
jre-openjdk / jre21-openjdk / jre17-openjdk / … | jre-openjdk (latest non-LTS) or jre21-openjdk (current LTS) |
LibreOffice optionally uses Java for Base (database UI) and some macros. The latest LTS (21 as of mid-2026) is the safe default. |
:: There are N providers available for cron (rarely seen) |
cronie / systemd-cron / fcron | cronie |
Arch’s traditional cron. You probably don’t need cron at all on this install (systemd timers do the job), but if something pulls it in, cronie is the safest pick. |
For any of these, just hitting Enter also works if default=1 happens to be the recommendation. Double-check by reading the chosen option name before pressing — the order of options can shift between Arch updates as new providers are added.
If a different prompt comes up that’s not in this table, the pacman / yay reference article has the full “how to read these” treatment.
Click to expand: what each package does
| Package | Role |
|---|---|
firefox |
The Firefox web browser. Wayland-native by default on recent versions; no extra config needed under Hyprland. |
nautilus |
GNOME’s Files file manager. Plays well with Hyprland because it’s a regular GTK app — no GNOME shell required. |
libreoffice-fresh |
Latest stable LibreOffice (Writer, Calc, Impress, Draw, Math, Base). The -fresh variant lags real-stable by a few months; pick libreoffice-still if you prefer a slower release cadence. |
gwenview |
KDE’s image viewer / lightweight editor. Strong EXIF and folder-browsing UX, opens almost any image format Qt understands. |
kate |
KDE’s GUI text editor. Useful as a fallback when you want a graphical editor outside of nvim. |
ark |
KDE archive manager. Reads/writes .zip, .7z, .tar.*, .rar (read), .iso, etc. |
okular |
KDE’s document viewer. PDFs, ePub, DjVu, comic-book archives. Annotation support is the cleanest in the open-source space. |
haruna |
Modern KDE video player built on mpv. Tabs, playlists, smart subtitle handling. |
swayimg |
Minimal Wayland-native image viewer (sxiv-style). Pairs well with a tiling compositor when you want a no-chrome image popup. |
flatpak |
The Flatpak runtime — sandboxed cross-distro app packaging. Many apps (Bottles, OBS, Heroic Games Launcher, …) are easiest to install from Flathub. |
desktop-file-utils |
Provides update-desktop-database. Required by some Wayland app launchers (anyrun, fuzzel, walker) to index .desktop entries; safer to install up front. |
discover (AUR) |
KDE’s graphical software center. Can install/update native pacman packages and Flatpak apps from Flathub in one UI. Pulls in some KDE Frameworks deps but is the friendliest Flatpak GUI under Hyprland. |
flatpak ships without any configured remote. Add Flathub once, system-wide, so Discover and flatpak install both see it:
sudo flatpak remote-add --if-not-exists flathub \
https://flathub.org/repo/flathub.flatpakrepoNothing to enable — flatpak has no daemon. The runtime kicks in only when you actually run a Flatpak app.
Installing a handful of KDE apps under Hyprland pulls in Qt and a slice of KDE Frameworks (KF6) — about 200–300 MB of dependencies. That’s expected and harmless; the libraries just sit on disk until an app loads them. You don’t need to install plasma-desktop or any other shell.
8. Install a login manager (SDDM)
Caelestia doesn’t ship one. The project’s README recommends greetd + tuigreet; I’m using SDDM instead because it matches the KDE apps installed above and renders a graphical login screen instead of a TTY.
sudo pacman -S sddm qt6-svg qt6-virtualkeyboard| Package | Role |
|---|---|
sddm |
The Simple Desktop Display Manager — login greeter that probes installed *.desktop session files (Hyprland ships one) and lets you pick a session from the dropdown before authenticating. |
qt6-svg |
SVG rendering for the SDDM theme. Required by the default Breeze theme and almost every third-party theme. |
qt6-virtualkeyboard |
On-screen keyboard support. Optional for desktops but harmless and recommended if you might use this install on a touch device later. |
xorg-server + a few X11 deps
On current Arch (SDDM 0.21.0-6 as of mid-2026), the sddm package pulls these as transitive dependencies:
| Package | Why it’s pulled in |
|---|---|
xorg-server |
SDDM’s default greeter renders via an X.Org server, not directly via DRM/KMS. Despite SDDM being marketed as Qt6/Wayland-aware, the greeter screen itself still runs under X11 on current Arch packaging. |
xorg-xauth |
X11 cookie/authority management — required by xorg-server. |
xf86-input-libinput |
libinput → X11 input driver, so the SDDM greeter sees your keyboard and mouse. |
libxmu |
Misc X utilities, an xorg-server dep. |
About ~40 MB of X11 stack you can’t avoid. This is mandatory on current Arch packaging — not optional. The good news: the session SDDM launches (Hyprland) is still pure Wayland, so the X server only runs for the few seconds of greeter rendering during login. After you authenticate, X is gone and Hyprland is running natively on Wayland.
If you want to avoid pulling X11 entirely, use greetd + tuigreet instead (next callout) — that’s a TTY-based greeter and stays Wayland-pure end-to-end.
Enable SDDM so it starts at every boot:
sudo systemctl enable sddm.serviceYou should see one Created symlink '/etc/systemd/system/display-manager.service' → '/usr/lib/systemd/system/sddm.service' line — that’s the proof SDDM is registered as the system’s display manager.
Verify it’s set up to start at boot:
systemctl is-enabled sddm.service # should print: enabledDon’t systemctl start sddm.service right now — that would try to seize your current TTY (or fight with your SSH session). The reboot in Step 9 is the right time for SDDM to start.
When caelestia-meta installed hyprland, the package dropped /usr/share/wayland-sessions/hyprland.desktop. SDDM scans that directory on every login screen render, so Hyprland appears in the session dropdown automatically — no extra config needed. Caelestia also drops its own session entry; pick whichever you prefer (they both launch Hyprland; Caelestia’s variant pre-sets a few env vars for the bundled status bar).
If you’d rather have the project-recommended minimal TTY greeter instead of SDDM, swap the install + service-enable steps with:
sudo pacman -S greetd greetd-tuigreet
sudo systemctl enable greetd.service…and write /etc/greetd/config.toml per the Caelestia README. Only enable one display manager at a time — having both sddm.service and greetd.service enabled causes a fight on VT1 that nobody wins.
9. Reboot into the desktop
sudo rebootIf you’ve been running the install over SSH, this will close your session — that’s normal. Walk to the laptop’s physical display + keyboard for the first graphical login. SDDM renders on the local seat, not over SSH; you cannot drive the first login from a remote shell (see the SSH vs local seat note above).
The expected sequence on the screen:
Click to expand: stage-by-stage walkthrough
| Stage | What you should see | What’s happening under the hood |
|---|---|---|
| 1 | GRUB menu — Arch is the only entry; press Enter or wait the default timeout | UEFI firmware booted GRUB from the ESP |
| 2 | Please enter passphrase for disk … (cryptroot): |
systemd-in-initramfs is asking sd-encrypt to unlock /dev/nvme0n1p2 |
| 3 | A brief pause (~2–3 s) after correct passphrase | Kernel mounts the @ Btrfs subvolume as /, real systemd takes over, services start |
| 4 | SDDM graphical login screen appears on the local display | This is the moment-of-truth — proof Section 8 worked. The greeter is X11-backed (xorg-server briefly active), about to launch a Wayland session |
| 5 | Session dropdown in the bottom corner | Shows “Hyprland” and possibly “Caelestia” — both launch Hyprland with the same configs (Caelestia’s variant pre-sets a few env vars for the bundled status bar). Pick either. |
| 6 | Type your user password → Enter | SDDM authenticates via PAM, kills xorg-server, hands over the seat to Hyprland |
| 7 | Brief flicker, then Caelestia bar appears across the top, your wallpaper renders | Hyprland is up, Quickshell is rendering the status bar, your dGPU is back in D3cold (battery-friendly idle) |
If anything weird happens between SDDM and the bar fully rendering — black screen, fallback to TTY, SDDM crashes back to its login — see the troubleshooting section at the bottom of the article.
Part 5 — Verifying everything ended up where it should
What the first login looks like (and the one cosmetic error you’ll probably see)
When you authenticate at the SDDM greeter and Hyprland starts, expect to see:
- A black desktop background — Caelestia doesn’t set a wallpaper out-of-the-box. The bar, side panels, and Quickshell widgets render correctly on top of black; that’s the default fallback because Caelestia’s own wallpaper layer simply hasn’t been told which file to render yet. Not a bug — just an empty wallpaper slot. Fix in the “Setting your first wallpaper” callout below.
A trap worth knowing about before you try to set a wallpaper (or anything else that talks to the compositor): caelestia wallpaper, hyprctl, hyprlock, and most other Hyprland-aware tools fail when run from an SSH session. The error looks like:
File "/usr/lib/python3.14/site-packages/caelestia/utils/hypr.py", line 13, in message
sock.connect(socket_path)
FileNotFoundError: [Errno 2] No such file or directory
…or for hyprctl:
HYPRLAND_INSTANCE_SIGNATURE not set! (is hyprland running?)
Why: Hyprland’s IPC socket lives at $XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket.sock, and both of those env vars are only set inside processes Hyprland itself spawned. An SSH session gets a fresh environment with neither set, so the socket path resolves to garbage and clients fail to connect. Same architectural reason Hyprland itself can’t start over SSH — the compositor and its IPC are bound to the local seat.
Fix: open a foot terminal inside the Hyprland session (the default Caelestia keybind is Super + Return) and run the command there. From inside the session, the env vars are set automatically and the socket exists.
Workaround for the determined — if you really need to drive caelestia or hyprctl from SSH, export the env vars by hand first:
export XDG_RUNTIME_DIR=/run/user/$(id -u)
export HYPRLAND_INSTANCE_SIGNATURE=$(ls "$XDG_RUNTIME_DIR/hypr/" | head -1)
# Now caelestia / hyprctl find the socket
caelestia wallpaper -r ~/Pictures/Wallpapers/The cleaner mental model: SSH for headless work (package installs, file edits, system inspection, snapshot management), the local Hyprland session for anything that talks to the compositor (wallpaper, screen lock, audio routing, window layouts).
Important correction: an earlier version of this section claimed Caelestia’s Quickshell-based bar also handles the wallpaper layer. That’s wrong. Caelestia delegates wallpaper rendering to an external daemon — specifically awww (a fork of swww that the Caelestia maintainers prefer). The Quickshell shell decides which wallpaper to display and sends the path to the daemon via IPC; the daemon does the actual decode + Wayland layer-shell rendering.
The full chain on a working install:
caelestia wallpaper -f IMG # CLI wrapper
↓
~/.local/state/caelestia/wallpaper/path.txt # state file written
↓
awww img IMG # daemon called (via Quickshell.execDetached)
↓
awww-daemon # claims Wayland BACKGROUND layer-shell surface
↓
your monitor # pixels appear
If any step in that chain is missing or broken, you get the symptoms in the recovery section below.
To actually set a wallpaper, use the caelestia wallpaper subcommand. Its flags (per caelestia wallpaper --help):
Click to expand: what each flag does
| Flag | What it does |
|---|---|
-f, --file FILE |
Set a specific image as the wallpaper |
-r, --random [DIR] |
Pick a random image from DIR (defaults to whatever Caelestia is configured to scan) |
-p, --print [PATH] |
Preview the color scheme an image would generate, without switching to it |
-n, --no-filter |
Don’t reject images smaller than your monitor (useful for stylized low-res art) |
-t, --threshold N |
Minimum percentage of monitor size for the image to be accepted |
-N, --no-smart |
Don’t auto-update the desktop color scheme based on the wallpaper’s dominant color |
Common patterns:
# Set a specific wallpaper
caelestia wallpaper -f ~/Pictures/Wallpapers/sunset.jpg
# Random from a folder — the typical "I cloned a wallpapers repo" workflow
caelestia wallpaper -r ~/Pictures/Wallpapers/
# Preview the color scheme without committing — useful for picking
caelestia wallpaper -p ~/Pictures/Wallpapers/sunset.jpg
# Set without auto-retheming the bar
caelestia wallpaper -f ~/Pictures/Wallpapers/sunset.jpg -NWhen you run -f or -r, two things happen at once:
- The wallpaper appears on the background layer (replacing the black fallback).
- Caelestia’s
python-materialyoucolorlibrary extracts the dominant color from the image and re-themes the bar, widgets, and accent colors to match. This is the Material You-style coordinated look. If you don’t want this, add-N.
To make this your default on every login, add a line to ~/.config/caelestia/hypr-user.conf (or to a stow-managed personal overlay — see Part 10 of the pacman/yay article):
exec-once = caelestia wallpaper -r /home/evanns/Pictures/Wallpapers
That way every Hyprland session starts with a random wallpaper from your collection (and a fresh accent-color scheme to match). Use an absolute path — exec-once lines don’t always expand ~.
If caelestia wallpaper -f reports an image is too small, override with -n (no size filter). If you have a fixed favorite, use -f instead of -r.
After hitting every layer of failure (no daemon installed, daemon not finding Wayland, picker scanning the wrong directory, mid-session env changes not propagating), this is what your ~/.config/caelestia/hypr-user.conf needs to contain to make the whole wallpaper chain self-starting on every reboot:
env = CAELESTIA_WALLPAPERS_DIR,/home/<your-username>/Pictures/Wallpapers
exec-once = awww-daemon
exec-once = caelestia wallpaper -r /home/<your-username>/Pictures/Wallpapers -n
Substitute <your-username> with your actual home directory name (e.g., evanns). The order matters and the lines do specific things:
| Line | What it does |
|---|---|
env = CAELESTIA_WALLPAPERS_DIR,… |
Tells the Caelestia shell (Quickshell) where to scan for wallpapers when the launcher’s >wallpaper picker is opened. The shell reads this env var at startup; without it, the picker scans whatever default GlobalConfig.paths.wallpaperDir is and shows “no wallpapers found” if your collection isn’t there. |
exec-once = awww-daemon |
Starts the wallpaper-rendering daemon. This is the daemon that actually paints pixels — Caelestia/Quickshell only decides which wallpaper to display; awww-daemon does the rendering. Must be installed (yay -S swww resolves to awww which provides swww’s interface). |
exec-once = caelestia wallpaper -r DIR -n |
Picks a random wallpaper from your folder at login and tells awww to paint it. The -n flag skips the size filter (which can over-reject wallpapers smaller than your monitor). |
Append all three lines if you don’t have them yet:
cat >> ~/.config/caelestia/hypr-user.conf <<'EOF'
# Wallpaper chain — picker dir + daemon + initial random wallpaper
env = CAELESTIA_WALLPAPERS_DIR,/home/<your-username>/Pictures/Wallpapers
exec-once = awww-daemon
exec-once = caelestia wallpaper -r /home/<your-username>/Pictures/Wallpapers -n
EOFThen verify:
grep -E '^(exec-once|env)' ~/.config/caelestia/hypr-user.confOn every future reboot or fresh login: Hyprland reads hypr-user.conf, exports the env var into the spawn environment, fires exec-once lines top-to-bottom (awww-daemon before caelestia wallpaper, which is the order you need), and the wallpaper appears within a couple seconds of session start. Zero manual work.
A full troubleshooting walkthrough for the most common wallpaper failure modes — each step diagnoses one layer of the chain so you can stop as soon as you find the broken piece.
Symptom 1: Black background, no wallpaper paints even after caelestia wallpaper -f
The wallpaper daemon (awww-daemon) isn’t running. Test:
pgrep -af awww-daemonIf empty, install (if needed) and start:
yay -S swww # resolves to "awww" which provides swww — same end result
awww-daemon & disown
caelestia wallpaper -f ~/Pictures/Wallpapers/<some-image>.jpg -nA wallpaper should appear immediately. If awww-daemon panics with failed to connect to socket, see Symptom 4 below.
Symptom 2: >wallpaper picker says “no wallpapers found, try putting some in ~/Pictures/Wallpapers”
The picker’s scan directory doesn’t match where your wallpapers actually live. The picker reads Paths.wallsdir, which resolves to $CAELESTIA_WALLPAPERS_DIR if set, otherwise GlobalConfig.paths.wallpaperDir. If neither is configured, the scan falls through to an unset/empty path.
Fix: set the env var via hypr-user.conf:
echo 'env = CAELESTIA_WALLPAPERS_DIR,/home/evanns/Pictures/Wallpapers' >> ~/.config/caelestia/hypr-user.conf
hyprctl reloadThen restart the Caelestia shell so it picks up the new env (env changes in hypr-user.conf apply to processes Hyprland spawns, not to ones already running):
caelestia shell -k 2>/dev/null # kill the running shell
caelestia shell -d & # restart as daemon
disownRe-open the launcher, type >wallpaper, and the picker should now show your collection.
Symptom 3: caelestia wallpaper -r DIR -n exits 0 silently but nothing changes on screen
You’re running the command from a shell that doesn’t have Hyprland’s env vars set — usually because you’re SSH’d in. The CLI exits cleanly because with -n it skips the monitor-size IPC query, but the actual paint call to awww silently fails because the IPC socket isn’t reachable.
Fix: run the command from inside the Hyprland session. Press Super + Return at the laptop keyboard to open a foot terminal that is a child of Hyprland (inherits the env vars), and run the command there.
If you really need to do it from SSH, export the env vars manually first:
export XDG_RUNTIME_DIR=/run/user/$(id -u)
export HYPRLAND_INSTANCE_SIGNATURE=$(ls "$XDG_RUNTIME_DIR/hypr/" | head -1)
export WAYLAND_DISPLAY=wayland-1
caelestia wallpaper -f ~/Pictures/Wallpapers/<some-image>.jpg -nNote all three env vars — XDG_RUNTIME_DIR, HYPRLAND_INSTANCE_SIGNATURE, and WAYLAND_DISPLAY. Missing WAYLAND_DISPLAY causes awww-daemon to panic with failed to connect to socket looking for wayland-0 instead of the actual wayland-1 socket.
Symptom 4: awww-daemon panics with “failed to connect to socket”
Missing WAYLAND_DISPLAY env var. From the shell where you’re trying to start the daemon:
export WAYLAND_DISPLAY=wayland-1 # or whatever ls /run/user/$(id -u)/wayland-* shows
awww-daemon & disownIf awww-daemon instead complains about a missing cache dir (~/.cache/awww: No such file or directory), that’s a non-fatal warning — the daemon creates the directory on first paint. Continue.
Symptom 5: Wallpaper paints when set manually but not on next login
Your ~/.config/caelestia/hypr-user.conf is missing the exec-once = awww-daemon line (or it’s after the caelestia wallpaper line). Hyprland fires exec-once lines in declaration order — the daemon needs to be running before the wallpaper command tries to paint.
Verify with:
grep -E '^(exec-once|env)' ~/.config/caelestia/hypr-user.confExpected (in this order):
env = CAELESTIA_WALLPAPERS_DIR,/home/<user>/Pictures/Wallpapers
exec-once = awww-daemon
exec-once = caelestia wallpaper -r /home/<user>/Pictures/Wallpapers -n
If awww-daemon appears after caelestia wallpaper, edit the file with nvim and reorder. If it’s missing entirely, append:
sed -i '/exec-once = caelestia wallpaper/i exec-once = awww-daemon' ~/.config/caelestia/hypr-user.conf(The sed inserts awww-daemon immediately before the caelestia wallpaper line. Run it once; running it twice would create duplicate lines.)
- Caelestia’s status bar across the top with the time, system tray, and workspace indicators rendered.
- Side panels (notification feed, calendar, control center, etc., depending on which keybinds you trigger).
- Default Hyprland window borders around any foot terminals you open with
Super+Return.
$cConf/hypr-vars.conf
A small upstream Caelestia bug: on some installs the bundle’s hyprland.conf references a hypr-vars.conf file that isn’t shipped, and Hyprland prints any failed source = directive as a red overlay at the bottom of the screen. The line that triggers it looks like:
source = $cConf/hypr-vars.conf
Everything else loads fine — bar, widgets, keybinds, animations all work — but the overlay is visually noisy. To clear it:
Open the Caelestia-side Hyprland config (this is the real file; your
~/.config/hypr/hyprland.confis a symlink to it):nvim ~/.local/share/caelestia/hypr/hyprland.confFind the line
source = $cConf/hypr-vars.confand comment it out by prefixing with#:# source = $cConf/hypr-vars.confSave, exit nvim, and reload Hyprland’s config without restarting your session:
hyprctl reload
The error overlay disappears immediately. Your terminals and any open windows stay exactly where they were — hyprctl reload re-parses the config in place, it doesn’t kill the session.
If Caelestia later ships a fix (or you cd ~/.local/share/caelestia && git pull and the line gets re-added because upstream added it back without the missing file), repeat the comment-out. You can also just touch ~/.config/hypr/hypr-vars.conf to create an empty file at the expected path — that satisfies the source directive without you having to remember to re-comment after each pull. Both fixes are valid; commenting is simpler, the empty-file trick is more git pull-resistant.
Customizing Hyprland after Caelestia is installed (hyprlang vs. Lua)
Starting with Hyprland 0.55 (mid-2026), the project officially deprecated its custom hyprlang config format in favor of embedded Lua. The canonical config path is now ~/.config/hypr/hyprland.lua instead of hyprland.conf. Hyprland still reads the legacy hyprland.conf for backwards compatibility — and that’s what Caelestia ships — but new features land in the Lua API first, and the project’s wiki examples are now all in Lua.
What this means for your install:
- Caelestia bundles
~/.local/share/caelestia/hypr/hyprland.conf(hyprlang). This is still fully supported. You do not need to migrate Caelestia’s bundle to Lua — the legacy parser handles it. - Your personal overrides can be written in either hyprlang (
~/.config/caelestia/hypr-user.conf, the slot Caelestia exposes) or Lua (~/.config/hypr/hyprland.lua, the modern Hyprland-native path). Both files are loaded automatically — they coexist without interfering. - For anything beyond simple
bind/exec-once/envlines — loops, functions, conditional logic, runtime event handlers — Lua is much cleaner.
Worth being precise about, because they’re easy to conflate:
| Layer | What’s on your system | Status |
|---|---|---|
| Hyprland binary | hyprland-0.55.1-1 (current Arch release as of mid-2026) |
Modern. Supports both hyprlang and Lua simultaneously. |
| Caelestia’s bundled config format | hyprlang (~/.local/share/caelestia/hypr/hyprland.conf) |
Legacy format. Still fully parsed by Hyprland 0.55+ for backwards compatibility. |
| The personal-overrides slot Caelestia exposes | ~/.config/caelestia/hypr-user.conf (hyprlang) |
Legacy format, sourced by the bundle’s main config. |
| The Hyprland-native modern slot | ~/.config/hypr/hyprland.lua (you create this) |
Modern Lua format. Loaded automatically alongside the legacy files. |
So Caelestia is not “on an older Hyprland” — you’re running the current Hyprland release. Caelestia just hasn’t migrated its config file from hyprlang to Lua yet. Likely reasons:
- The hyprlang → Lua transition is brand-new (0.55 is a recent release). Rewriting hundreds of lines of bundled config in a freshly-stable API takes time and testing.
- hyprlang still works perfectly. There’s no functional pressure to migrate — users get identical behavior either way.
- The Caelestia maintainers may be waiting for the Lua API to settle before committing to a rewrite.
Expect Caelestia to migrate eventually (probably within a release or two). When they do, cd ~/.local/share/caelestia && git pull will pull down a Lua-formatted bundle, and Hyprland’s loader will pick it up seamlessly — no action needed on your end other than verifying nothing broke after the pull.
To check what versions you actually have:
pacman -Qi hyprland | grep -E '^(Name|Version)'
pacman -Qi caelestia-shell | grep -E '^(Name|Version)'
hyprctl version | head -5
cd ~/.local/share/caelestia && git log --oneline -1Four lines: Hyprland binary version, Caelestia shell version, what hyprctl reports (sometimes diverges from package version on -git installs), and the latest commit you have from the Caelestia repo.
The right place to put your overrides (cardinal rule)
Never edit ~/.local/share/caelestia/hypr/*.conf directly. That’s Caelestia upstream — cd ~/.local/share/caelestia && git pull will clobber any changes you make there. Two safe slots for personal customization:
| File | Format | Use for |
|---|---|---|
~/.config/caelestia/hypr-user.conf |
hyprlang (legacy) | Simple overrides that match the bundle’s style: a few keybinds, env vars, exec-once lines. Caelestia explicitly sources this file. |
~/.config/hypr/hyprland.lua |
Lua (modern, Hyprland 0.55+) | Anything you want loops, functions, event handlers, or programmatic logic for. Loaded automatically by Hyprland alongside the legacy config. |
~/.config/caelestia/hypr-vars.conf |
hyprlang | Variable overrides Caelestia exposes ($gap, $borderwidth, accent colors). Vars only, not full directives. |
You can use both hypr-user.conf and hyprland.lua together — they’re additive, not exclusive. Many users keep the existing 3-line hyprlang recipe in hypr-user.conf (env + two exec-once) and put everything else in Lua.
The Lua API at a glance
Hyprland exposes a global hl table with these methods:
Click to expand: the API at a glance
| API call | Purpose |
|---|---|
hl.config({ section = { key = value } }) |
Set any config section — general, decoration, animations, input, dwindle, etc. |
hl.bind(MODS .. " + " .. KEY, hl.dsp.exec_cmd("...")) |
Bind a key combination to a dispatcher. |
hl.dsp.exec_cmd(cmd), hl.dsp.workspace(n), hl.dsp.window.close(), hl.dsp.movetoworkspace(n) |
Built-in dispatchers — exec a shell command, switch workspace, close window, etc. |
hl.monitor({ output, mode, position, scale }) |
Configure a monitor — equivalent to monitor = in hyprlang. |
hl.env(name, value) |
Set an env var for child processes. Equivalent to env = NAME,VALUE. |
hl.on("hyprland.start", fn) |
Run fn once at session start. Equivalent to exec-once. Other events fire on workspace change, window open, etc. |
hl.exec_cmd(cmd) |
Execute a shell command. Use inside event handlers. |
hl.window_rule({ name, match = { class, title }, ...props }) |
Per-app window behavior. Replaces windowrulev2. |
hl.workspace_rule({ workspace, gaps_in, gaps_out, ... }) |
Per-workspace rules. |
hl.gesture({ fingers, direction, action }) |
Touchpad gestures. |
hl.device({ name, sensitivity, ... }) |
Per-input-device overrides. |
hl.curve(name, { type = "bezier", points = ... }), hl.animation({...}) |
Custom animation curves and per-element timing. |
Full reference: wiki.hypr.land/Configuring/Start/.
Worked example: porting the wallpaper recipe from hyprlang to Lua
The 3-line hyprlang recipe (in hypr-user.conf):
env = CAELESTIA_WALLPAPERS_DIR,/home/evanns/Pictures/Wallpapers
exec-once = awww-daemon
exec-once = caelestia wallpaper -r /home/evanns/Pictures/Wallpapers -n
Equivalent in Lua (in ~/.config/hypr/hyprland.lua):
hl.env("CAELESTIA_WALLPAPERS_DIR", "/home/evanns/Pictures/Wallpapers")
hl.on("hyprland.start", function()
hl.exec_cmd("awww-daemon")
hl.exec_cmd("caelestia wallpaper -r /home/evanns/Pictures/Wallpapers -n")
end)Both forms produce identical session behavior. The Lua version becomes useful once you add conditional logic — e.g., “use a different wallpaper folder on Mondays,” “skip wallpaper auto-start if a specific file exists” — none of which is expressible in hyprlang.
A larger Lua example — keybinds, window rules, env vars, event handlers
For readers who want a full personal Lua override file as a starting template:
-- ~/.config/hypr/hyprland.lua
-- Personal overrides on top of Caelestia. Hyprland 0.55+ native Lua format.
local mainMod = "SUPER"
-- ===== Keybinds (programmatic — no copy-paste 9 lines for workspace switching) =====
local apps = {
{ mods = mainMod, key = "B", cmd = "firefox" },
{ mods = mainMod, key = "E", cmd = "nautilus" },
{ mods = mainMod .. " SHIFT", key = "Return", cmd = "foot" },
{ mods = mainMod, key = "P", cmd = "pavucontrol" },
}
for _, app in ipairs(apps) do
hl.bind(app.mods .. " + " .. app.key, hl.dsp.exec_cmd(app.cmd))
end
-- Workspace switching 1-9 + move-to-workspace, generated in a loop
for i = 1, 9 do
hl.bind(mainMod .. " + " .. i, hl.dsp.workspace(tostring(i)))
hl.bind(mainMod .. " SHIFT + " .. i, hl.dsp.movetoworkspace(tostring(i)))
end
-- Roll a new random wallpaper on demand
hl.bind(mainMod .. " + W", hl.dsp.exec_cmd(
"caelestia wallpaper -r /home/evanns/Pictures/Wallpapers -n"
))
-- ===== Window rules =====
hl.window_rule({
name = "float-pavucontrol",
match = { class = "^(pavucontrol)$" },
float = true,
})
hl.window_rule({
name = "send-rqt-windows-to-ws3",
match = { class = "^(rqt_.*)$" }, -- ROS rqt tools
workspace = "3",
})
-- ===== Env vars (NVIDIA on Wayland + Electron Wayland hint) =====
hl.env("CAELESTIA_WALLPAPERS_DIR", "/home/evanns/Pictures/Wallpapers")
hl.env("LIBVA_DRIVER_NAME", "nvidia")
hl.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia")
hl.env("NVD_BACKEND", "direct")
hl.env("ELECTRON_OZONE_PLATFORM_HINT", "auto")
-- ===== Session startup (replaces exec-once) =====
hl.on("hyprland.start", function()
hl.exec_cmd("awww-daemon")
hl.exec_cmd("caelestia wallpaper -r /home/evanns/Pictures/Wallpapers -n")
end)Reload after editing:
hyprctl reloadMost changes apply instantly. The exceptions are the same as for hyprlang: hl.env(...) only affects newly-spawned processes, and certain Caelestia-shell-rendered things (bar, picker) may need caelestia shell -k && caelestia shell -d & to pick up env changes.
Path A vs. Path B — which to pick
| Path | What it looks like | When to use |
|---|---|---|
A — Keep the hyprlang hypr-user.conf + add a small Lua file alongside |
The 3-line wallpaper recipe stays in hypr-user.conf; new things (keybinds, window rules, event handlers) go in hyprland.lua. Both load automatically. |
Recommended for the typical reader. Lowest friction. Lets you adopt Lua incrementally instead of rewriting everything at once. |
B — Move everything to hyprland.lua and leave hypr-user.conf empty |
All personal overrides as Lua. hypr-user.conf exists but is empty (Caelestia still sources it; an empty file is fine). |
When you’re comfortable with Lua and want a single file for all your customization. Cleaner long-term. |
For your install, Path A is the natural next step — you already have the working hyprlang wallpaper recipe; adding a ~/.config/hypr/hyprland.lua for any new customization just means you’re using the modern API for new work. Migrate the rest gradually if and when you want.
Discovering what’s already configured
hyprctl getoption general:gaps_in # current value of a setting
hyprctl getoption decoration:rounding
hyprctl binds # all your active keybinds — Caelestia's + yours
hyprctl monitors # detected displays with res/scale/refresh
hyprctl clients # currently open windows (use for class: in window rules)
hyprctl devices # input devices (touchpad, keyboard)These work regardless of whether the config came from hyprlang or Lua — Hyprland normalizes both into the same in-memory state.
Verification commands
Open a foot terminal — Caelestia’s default keybind is Super+Return — and run:
# Filesystem & encryption
lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINTS
cryptsetup status cryptroot # active LUKS mapping
findmnt -t btrfs # all five Btrfs mountpoints (@, @home, @snapshots, @var_log, @swap)
sudo btrfs subvolume list / # same five subvolumes, listed by ID
# Swap + hibernation
swapon --show # zram0 (pri 100) + /swap/swapfile (pri -1)
zramctl # zstd-compressed zram device
# Snapshots — should now show MANY (one per pacman transaction during install)
sudo snapper -c root list # expect snapshots numbered well into the 40s if you followed every step
ls /.snapshots/ # numbered snapshot directories matching the list above
# Compositor + Wayland
hyprctl version # now prints "Hyprland, built from branch ..." — no more "INSTANCE_SIGNATURE not set"
hyprctl monitors # detected displays with resolution, refresh rate, scale
echo $WAYLAND_DISPLAY # something like "wayland-1" — proves you're in a Wayland session, not Xwayland
# Audio (PipeWire stack)
pactl info | grep 'Server Name' # "PulseAudio (on PipeWire 1.6.x)" — confirms pipewire-pulse is intercepting PA API
wpctl status # tree of audio devices, sinks, sources — your laptop speakers/headphone jack should be there
systemctl --user status pipewire wireplumber pipewire-pulse | grep Active
# all three should be Active: active (running)
# GPU (on hybrid laptops — Dell Precision example)
nvidia-smi # confirms dGPU still alive and idle at ~0.5 W (or wakes briefly to query)
vainfo 2>/dev/null | head -5 # iGPU VA-API confirms working video decode
# Caelestia-specific
caelestia --version # CLI version
systemctl --user status caelestia-shell.service 2>/dev/null || true
# may or may not exist depending on Caelestia version — fine either wayIf every command above returns sane output and the desktop looks like Caelestia (rounded windows, the status bar across the top, the cyan accent), the install is complete.