Skip to main content

Caelestia (Hyprland Desktop) — From Bare Arch to a Working Bar

arch linux
linux
caelestia
hyprland
sddm
pipewire
wayland
nvidia
lua
dotfiles
The desktop half of my Arch install: PipeWire audio, GPU drivers, an AUR helper, the Caelestia Hyprland bundle, KDE-Frameworks apps under Hyprland, SDDM as the login manager, the wallpaper-chain recipe that actually survives a reboot, and the new Hyprland 0.55 Lua override path. Assumes a working Arch base — bring your own LUKS+Btrfs install (or follow Article 03 to get there).
Author

Evanns Morales-Cuadrado

Published

May 18, 2026

What this is

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 itselfcaelestia-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.

Caveats from actually using it

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 .SRCINFO parsing failure I document below is real, and several other small papercuts (the missing hypr-vars.conf source = line that paints a red overlay on first login, the wallpaper-daemon-not-installed-by-default chain, the SSH-vs-local-seat trap with hyprctl) 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 of hyprland.conf / hyprland.lua.
  • What I’m doing next: my next install will skip Caelestia and follow the upcoming Custom Hyprland articlewaybar or ironbar instead 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-firmware

When 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.service

Then 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 sources

If 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.

Two common failure modes after install
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-utils
Click 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.service

The --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
nvim

The 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 laptop

Then 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-utils

Verify 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-driver

The 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.

On a hybrid: 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 idle

If 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)
Verify dGPU dynamic power management is on (it should be, by default)

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/power

On 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 while nvidia-smi is 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-utils
Click 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:

vainfo

You 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.

Nothing to enable here

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.

Hybrid laptops (Intel + NVIDIA, AMD + NVIDIA): install both blocks

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=1 themselves (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 libglvnd provider — pick nvidia-utils. mesa and nvidia-utils both provide pieces of libGL; the GLVND dispatcher at runtime routes calls to whichever GPU each app is actually rendering on.
  • DKMS compiles the nvidia kernel modules against your current kernel (~30–60 seconds, runs inside pacman’s post-install hook). linux-headers from pacstrap is what makes this work — without it, the compile fails loudly. The -open-dkms variant 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.conf modules-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 env lines only take effect for apps started after the change. Either log out and log back in, or hyprctl reload and 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 blender

    prime-run is a tiny wrapper script that comes with nvidia-prime (worth pacman -S nvidia-prime if 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) and GBM_BACKEND=nvidia-drm (sets the GBM allocator). With nvidia-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=yay
Do not move the repo after running install.fish

install.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).

Common failure: 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:

  1. 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 caelestia
  2. Install 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 uwsm
  3. Install the AUR components individually, omitting caelestia-meta — yay caches the source tarballs even when it cleans the built .pkg.tar.zst artifacts, 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 qtengine

    For the yay prompts: N to cleanBuild (cache is good), N to diffs, N to PKGBUILDs to edit, N to remove make deps after install, Y to “Proceed with installation?”. (See the pacman + yay reference article for full prompt explanations.)

  4. Re-run install.fish so it finishes the config-symlinking step now that hyprctl and caelestia exist:

    ~/.local/share/caelestia/install.fish --aur-helper=yay

    At the prompts: pick 1 for “backup already exists” (your ~/.config was backed up on the first attempt), and Y to 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 \
  discover
Heads up: discover 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 reposudo 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.

Provider prompts you’ll see during this install — what to pick

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.
Add Flathub before opening Discover

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.flatpakrepo

Nothing to enable — flatpak has no daemon. The runtime kicks in only when you actually run a Flatpak app.

What about the rest of the KDE stack?

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.
Expect SDDM to pull in 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.service

You 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: enabled

Don’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.

How SDDM finds Hyprland

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).

Prefer greetd + tuigreet?

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 reboot

If 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.
Run Hyprland-interacting commands inside the session, not over SSH

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).

Setting your first wallpaper (and how Caelestia’s wallpaper layer actually works)

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 -N

When you run -f or -r, two things happen at once:

  1. The wallpaper appears on the background layer (replacing the black fallback).
  2. Caelestia’s python-materialyoucolor library 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.

The three-line recipe that makes wallpapers Just Work from first login

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
EOF

Then verify:

grep -E '^(exec-once|env)' ~/.config/caelestia/hypr-user.conf

On 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.

Recovery: “black background after login,” “no wallpapers found” in the picker, or both

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-daemon

If 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 -n

A 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 reload

Then 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
disown

Re-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 -n

Note 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 & disown

If 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.conf

Expected (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.
If you see a “config error” overlay at the bottom of the screen referencing $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:

  1. Open the Caelestia-side Hyprland config (this is the real file; your ~/.config/hypr/hyprland.conf is a symlink to it):

    nvim ~/.local/share/caelestia/hypr/hyprland.conf
  2. Find the line source = $cConf/hypr-vars.conf and comment it out by prefixing with #:

    # source = $cConf/hypr-vars.conf
  3. Save, 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)

Hyprland 0.55+ uses Lua as its primary config language

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 / env lines — loops, functions, conditional logic, runtime event handlers — Lua is much cleaner.
Caelestia version vs. Caelestia config format — they’re different things

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:

  1. 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.
  2. hyprlang still works perfectly. There’s no functional pressure to migrate — users get identical behavior either way.
  3. 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 -1

Four 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 reload

Most 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 way

If 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.