The only way to learn mathematics is to do mathematics, Paul Halmos

Following the base system setup, the next critical phase is establishing the graphical environment. Unlike traditional desktop environments, a modern Wayland setup requires careful integration of the compositor, display manager, and user interface shell.
This guide provides three automated scripts to bridge that gap: installing Niri, a unique scrollable-tiling Wayland compositor; configuring greetd with tuigreet for a minimal, secure login experience; and deploying Noctalia Shell, a sophisticated Quickshell-based desktop interface. Together, these components create a lean, efficient, and highly customizable graphical session from the ground up.
This script installs Niri, a unique scrollable-tiling Wayland compositor.
#!/usr/bin/env bash
# ── 02-niri.sh ────────────────────────────────────────────────────────────────
# Install Niri — a scrollable-tiling Wayland compositor.
#
# Niri concepts (different from i3):
# - No workspaces in the traditional sense — windows tile LEFT/RIGHT
# in an infinite horizontal scroll canvas.
# - Workspaces are VERTICAL: each column of windows is a workspace.
# - Overview (Super+O): bird's-eye view of all workspaces.
# - No fixed workspace count: Workspaces are created/destroyed dynamically
#
# Run: bash 02-niri.sh
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
echo "── 02-niri: installing Niri and Wayland compositor dependencies..."
# ── Niri + XWayland ───────────────────────────────────────────────────────────
# niri: the compositor itself (available in official Arch repos)
# xwayland-satellite: runs XWayland on-demand as a separate process.
sudo pacman -S --needed --noconfirm \
niri \
xwayland-satellite
# ── Wayland session support ───────────────────────────────────────────────────
# polkit-kde-agent: Polkit manages system-wide privileges for unprivileged processes.
# qt6-wayland: Qt6 apps run natively on Wayland instead of via XWayland
# brightnessctl: keyboard brightness keys (used in niri keybinds)
# playerctl: media key support (play/pause/next from keyboard)
# swayidle: idle management — lock screen and suspend after inactivity
# swaylock: lock screen
# SwayBG is a simple wallpaper setter
sudo pacman -S --needed --noconfirm \
polkit-kde-agent \
qt6-wayland \
qt5-wayland \
brightnessctl \
playerctl \
swayidle \
swaylock \
swaybg
# ── Create config directory ───────────────────────────────────────────────────
# The symlink script (05-symlinks.sh) will populate this with our dotfile.
# We create it now so niri can start even before symlinking.
mkdir -p ~/.config/niri
# ── Wayland session file ──────────────────────────────────────────────────────
# Check if niri installed its own .desktop session file (it should have)
if [[ -f /usr/share/wayland-sessions/niri.desktop ]]; then
echo " ✅ Niri session file found at /usr/share/wayland-sessions/niri.desktop"
else
# Fallback: create it manually so greetd can find the session
echo " ⚠️ Creating niri.desktop manually..."
sudo tee /usr/share/wayland-sessions/niri.desktop > /dev/null << 'SESSION'
[Desktop Entry]
Name=Niri # Displayed in display manager session chooser
Comment=A scrollable-tiling Wayland compositor
Exec=niri-session # The command to launch the compositor session
Type=Application # Marks this as an application entry
SESSION
fi
echo "✅ 02-niri: done"
echo " Next: bash 03-display-manager.sh"
greetd is a minimal (simple), compositor-agnostic (it works with any window manager) and flexible login manager daemon which does not make assumptions about what the user wants to launch, should it be console-based or graphical. It is Wayland-native (it does not force Xorg), modular, PAM-based (it uses standard Linux authentication), and uses a single TOML configuration file.
# Install greetd + tuigreet as the display manager (login screen).
#
# greetd is a minimal, compositor-agnostic display manager designed for
# Wayland. It does not carry GTK/Qt dependencies, does not assume a
# specific desktop, and starts sessions cleanly via PAM.
#
# Run: bash 03-display-manager.sh
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail # Exit on error, undefined vars, pipe failures
echo "── 03-display-manager: installing greetd and tuigreet..."
# ── Disable any existing display manager ──────────────────────────────────────
# Only one display manager can own the login. Disable others first.
for dm in sddm gdm lightdm lxdm; do
if systemctl is-enabled "$dm" &>/dev/null; then
# Returns 0 (success) if service is enabled
# Silenced with &>/dev/null (discards both stdout and stderr)
# We only care about the exit code
echo " ⚠️ Disabling existing display manager: $dm"
sudo systemctl disable "$dm"
fi
done
# ── Install ───────────────────────────────────────────────────────────────────
sudo pacman -S --needed --noconfirm greetd greetd-tuigreet
# greetd: The core display manager daemon
# greetd-tuigreet: Terminal UI greeter (separate binary)
# Flags: --needed: Skip packages already installed (idempotent), --noconfirm: Auto-answer yes (for scripting)
# ── greetd config ─────────────────────────────────────────────────────────────
# The config file at /etc/greetd/config.toml tells greetd which greeter to run.
# We write it here; 05-symlinks.sh will later replace it with a symlink to
# dotfiles/myArch/config/greetd/config.toml for version control.
#
# tuigreet flags used:
# --time show a clock in the greeter
# --remember remember the last username
# --sessions path to .desktop session files (greetd finds Niri here)
# --asterisks show * characters while typing password (security feedback)
sudo mkdir -p /etc/greetd
# ── Enable greetd ─────────────────────────────────────────────────────────────
# greetd replaces getty on VT1. We enable it as a systemd service.
sudo systemctl enable greetd
echo "✅ 03-display-manager: done — greetd enabled, will start on next boot"
echo " Next: bash 04-noctalia.sh"
[terminal] # Configures the virtual terminal where greetd runs
vt = 1
[default_session] # Defines what to run (greeter) and as which user
command = "tuigreet --time --remember --sessions /usr/share/wayland-sessions --asterisks --greeting 'Welcome back, nmaximo7'"
user = "greeter"
# command: The actual command to execute for the greeter; user: System user that runs the greeter (security isolation)
tuigreet \
--time \ # Show clock in greeter
--remember \ # Remember last username (cached in /var/cache/tuigreet)
--sessions /usr/share/wayland-sessions \ # Where to find .desktop session files
--asterisks \ # Show * when typing password (privacy vs feedback)
--greeting 'Welcome back, nmaximo7' # Custom greeting text
This script installs Noctalia Shell, a Quickshell-based desktop shell for Wayland. Quickshell is a declarative framework for building Wayland desktop shells using QML. Quickshell is like a “React for Wayland shells” - you describe your UI declaratively, and it handles the low-level Wayland communication.
Noctalia maintains its own fork (noctalia-qs).
#!/usr/bin/env bash
# ── 04-noctalia.sh ────────────────────────────────────────────────────────────
# Install Noctalia Shell — a Quickshell-based desktop shell for Wayland.
#
# Architecture (as of Noctalia v4.6+):
# noctalia-qs = Noctalia's own fork of the Quickshell runtime (AUR)
# noctalia-shell = the shell config/QML code that runs on noctalia-qs (AUR)
# noctalia-shell pulls noctalia-qs as a dependency automatically.
#
# IMPORTANT: noctalia-qs conflicts with quickshell and quickshell-git.
# Both must be removed before installing. This script handles that.
#
# Start command (in niri config.kdl):
# spawn-at-startup "noctalia-qs" "-c" "noctalia-shell"
# NOT "noctalia-shell" directly — the binary is noctalia-qs.
#
# Run: bash 04-noctalia.sh
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
echo "── 04-noctalia: installing Noctalia Shell..."
# ── Conflict resolution ───────────────────────────────────────────────────────
# noctalia-qs conflicts with quickshell and quickshell-git.
# We use paru -R (not pacman -R) because these are AUR packages —
# pacman -R can fail with "target not found" for AUR-installed packages
# even when pacman -Qi shows them as installed.
# --noconfirm + --nodeps removes them without asking about dependents.
for pkg in quickshell quickshell-git; do
if paru -Qi "$pkg" &>/dev/null 2>&1; then
# The script attempts three removal methods
echo " ⚠️ Removing conflicting package: $pkg"
# Method 1: paru -R (preferred for AUR packages)
paru -R --noconfirm --nodeps "$pkg" 2>/dev/null || \
# Method 2: sudo pacman -R (fallback for official packages)
sudo pacman -R --noconfirm --nodeps "$pkg" 2>/dev/null || \
# Method 3: Manual instruction if both fail
echo " ℹ️ Could not remove $pkg automatically — remove manually if install fails"
else
echo " ✅ $pkg not installed, no conflict"
fi
done
# ── Install from AUR ──────────────────────────────────────────────────────────
# noctalia-shell pulls noctalia-qs as a dependency.
# noctalia-qs builds from source — 10-30 min on first install.
# Subsequent updates use the build cache and are much faster.
echo " ⏳ Building noctalia-shell from AUR (may take 10-30 minutes)..."
paru -S --needed --noconfirm noctalia-shell
# ── Config directory ──────────────────────────────────────────────────────────
# ~/.config/noctalia/ is where Noctalia reads settings.json.
# 05-symlinks.sh will symlink dotfiles/myArch/config/noctalia/settings.json here.
mkdir -p ~/.config/noctalia
# ── Verify installation ───────────────────────────────────────────────────────
# The binary installed by noctalia-qs is `noctalia-qs` (not `noctalia-shell`).
# noctalia-shell is the config name passed with -c, not an executable.
echo
if command -v noctalia-qs &>/dev/null; then
echo " ✅ noctalia-qs binary found: $(which noctalia-qs)"
echo " ℹ️ Start with: noctalia-qs -c noctalia-shell"
echo " ℹ️ In niri config.kdl use: spawn-at-startup \"noctalia-qs\" \"-c\" \"noctalia-shell\""
else
echo " ⚠️ noctalia-qs not found in PATH — check paru output above"
fi
echo
echo "✅ 04-noctalia: done"
echo " Next: bash 05-symlinks.sh"