Before we start, a small disclaimer:
This began as a guide where I explained to myself how to install Arch Linux and Hyprland with the configuration I use. I wrote it in 2024, left it half-finished (the title carried an “[incomplete]” tag for two years, let’s be honest), and in 2026 I finally completed and updated it with the setup I use daily.
The reason is still the same: I’ve been running Arch with Hyprland as my daily driver for years, I’ve put real time into a configuration I like, and I keep slowly forgetting what I configured and how. This guide is a lifeline in case I ever have to reinstall from scratch. I’m publishing it because I hope it helps you too.
One important thing about how it’s written: I don’t want you to copy my configuration. I want you to reach a complete desktop understanding what need each piece covers, see what my choice is and why, and pick your own. Wherever there are reasonable alternative paths, I point them out.
In each section I’ll list sources and resources I used, in case they’re useful.
installing arch linux
I assume you already have a bootable Arch Linux USB, which is easy to make with balena etcher, rufus or dd. I also assume your machine boots with EFI.
If you’d rather skip this whole section, there’s archinstall, the guided installer that ships with the ISO. It’s a perfectly valid path and more and more people use it. I prefer the manual install because doing it teaches you which pieces make up your system, and when something breaks (with Arch it’s a when, not an if) you’ll know where to start looking.
Boot from the USB and set up keyboard and font:
loadkeys es
setfont ter-132b
Check the internet connection with:
ping -c 1 archlinux.org
Over ethernet you shouldn’t have connection problems. If you’re on wifi, it’s normal not to have internet because it’s not connected yet; use iwctl:
iwctl
# inside the iwctl console
device list
station INTERFACE_NAME scan
station INTERFACE_NAME get-networks
station INTERFACE_NAME connect SSID
exit
Now let’s set the clock:
timedatectl set-timezone Europe/Madrid
timedatectl set-ntp true
partitioning and formatting
This depends on your disk and how you want to install. Some people split root and home into different partitions; I’ve never seen the advantage for a personal machine, so I keep it simple with 3 partitions: EFI, swap and root.
Here’s your first path decision: the filesystem. I use ext4, which is boring and reliable. The alternative path is btrfs with snapper, which gives you system snapshots before every update (a very good life insurance on a rolling release). I always meant to try it; if it calls to you, this video by Ermanno explains it well: How to install Arch Linux with BTRFS & Snapper
.
Warning: don’t copy-paste the next block, adapt the partitions to your disk.
lsblk
# imagining you'll use /dev/sda, though it could be another
# disk like /dev/nvme0n1 or /dev/mmcblk0
# sda1 efi 512MB
# sda2 swap between 2GB and half your installed RAM
# sda3 root the remaining space
# to create the partitions, the easiest tool is cfdisk (gpt)
cfdisk
# efi -> 512MB type -> EFI System
# swap -> 2048MB type -> Linux swap
# root -> rest type -> Linux filesystem
# formatting
mkfs.ext4 /dev/_root_partition_
mkswap /dev/_swap_partition_
mkfs.fat -F 32 /dev/_efi_partition_
# mounting the partitions
mount /dev/_root_partition_ /mnt
mount --mkdir /dev/_efi_partition_ /mnt/boot
swapon /dev/_swap_partition_
base packages
Before downloading anything, I recommend reflector to pick the fastest mirrors, because sometimes you get one that crawls:
pacman -Syy
pacman -S reflector
# back up the list just in case
cp /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak
reflector -c "ES" -f 12 -l 10 -n 12 --save /etc/pacman.d/mirrorlist
Install the essential packages into the mount. Mind the microcode: intel-ucode if your CPU is Intel, amd-ucode if it’s AMD.
pacstrap -K /mnt base linux linux-firmware base-devel nano intel-ucode
Generate the mount instructions so partitions get mounted on every boot:
genfstab -U /mnt >> /mnt/etc/fstab
inside the chroot
Now what I call the actual Arch install, where we enter the mount point and start configuring the system:
arch-chroot /mnt
# time
ln -sf /usr/share/zoneinfo/Europe/Madrid /etc/localtime
hwclock --systohc
# locales: open /etc/locale.gen and uncomment en_US.UTF-8
# (plus your own language's line if you want it)
nano /etc/locale.gen
locale-gen
echo 'LANG=en_US.UTF-8' > /etc/locale.conf
# console keymap
echo 'KEYMAP=es' > /etc/vconsole.conf
# network
echo 'archpc' > /etc/hostname
cat >> /etc/hosts << 'HOSTS'
127.0.0.1 localhost
::1 localhost
127.0.1.1 archpc
HOSTS
# initramfs
mkinitcpio -P
# root password
passwd
# user (mine is zft9xgy, use yours) and sudo
pacman -S sudo
useradd -m zft9xgy
passwd zft9xgy
usermod -aG wheel zft9xgy
# uncomment the line %wheel ALL=(ALL:ALL) ALL
EDITOR=nano visudo
By the way, the previous version of this guide had a bug here that nobody reported in two years: it ran usermod -aG wheel on a user that wasn’t the one created two lines earlier. If you followed it to the letter and ended up without sudo, now you know why.
bootloader
I use GRUB. The minimalist alternative path is systemd-boot, which ships with systemd and configures in two lines; GRUB works for me out of habit and because it plays nice with other systems if there’s ever a dual boot.
pacman -S grub efibootmgr
mkdir /boot/efi
mount /dev/_efi_partition_ /boot/efi
# some motherboards can't handle a bootloader-id with spaces,
# in that case use --bootloader-id=GRUB
grub-install --target=x86_64-efi --bootloader-id="Arch Linux" --efi-directory=/boot/efi
grub-mkconfig -o /boot/grub/grub.cfg
network, bluetooth and audio before rebooting
Very important: right now you have internet because you’re in the install environment, but that environment disappears on reboot. If you don’t leave the networking bits installed and enabled, your brand new Arch will boot with no network and you’ll be back on the USB.
pacman -S networkmanager bluez bluez-utils pipewire pipewire-pulse pipewire-alsa wireplumber
systemctl enable NetworkManager
systemctl enable bluetooth
About audio: today the answer is PipeWire with WirePlumber, no debate. It replaces PulseAudio (the pipewire-pulse package keeps everything that expected PulseAudio working) and needs zero configuration for normal use.
If your GPU is AMD, like mine, the drivers are mesa and vulkan-radeon and that’s the whole story. If it’s NVIDIA, Hyprland works, but it’s the bumpiest path: read the NVIDIA page of the Hyprland wiki
first.
pacman -S mesa vulkan-radeon
Exit the chroot and reboot:
exit
umount -R /mnt
reboot
sources and resources
- Installation guide - ArchWiki : the official guide is very complete, though it can be overwhelming if you’re just starting.
- How to Install Arch Linux - It’s FOSS
first boot: yay and basic packages
Log in with your user and connect the network (nmtui is the most comfortable way for wifi). The first thing I install is yay, the AUR helper, because sooner or later you’ll want a package that’s not in the official repos:
sudo pacman -S git
git clone https://aur.archlinux.org/yay.git
cd yay
makepkg -si
And my terminal basics, where everyone will have their own list:
sudo pacman -S fish micro btop fastfetch unzip wget openssh
I use fish as my shell and micro as my terminal editor (yes, people who use neither vim nor neovim exist, and we live happily). btop for monitoring, and the rest are tools that always end up being needed.
If you’re into cybersecurity, there’s BlackArch as an additional repository with hundreds of pentesting tools. I don’t have it installed: for CTFs and labs I prefer installing the specific tools I need, keeping the daily system clean.
installing hyprland
Hyprland is a dynamic tiling Wayland compositor. In plain words: a window manager where windows arrange themselves in a mosaic, everything is keyboard-driven, and on top of that it looks good without too much fighting. It’s the reason many people (me included) end up on this side of Linux.
sudo pacman -S hyprland xdg-desktop-portal-hyprland hyprpolkitagent hyprpaper
Alongside the compositor there are three pieces that are not optional even if they look it: the portal (xdg-desktop-portal-hyprland), which is what makes screen sharing and file-picker dialogs work; the polkit agent (hyprpolkitagent), which is what shows you the password prompt when a graphical app needs privileges; and hyprpaper for the wallpaper.
starting the session: my no-display-manager path
Here comes another decision: how do you get into Hyprland when you power on?
My choice is the simplest possible one: no display manager at all. I log in on the text TTY and type Hyprland. One less package, one less service, one less login screen to configure. For a single-person machine I don’t need more.
The alternative paths, if you want a graphical login: greetd with tuigreet (minimalist, fits this setup nicely) or SDDM (the classic, heavier, more visual).
configuring hyprland in lua
And we reach the part that has changed the most since I started this guide. Hyprland used to be configured with a hyprland.conf file in its own format called hyprlang. Since version 0.55, hyprlang is deprecated: it still works for compatibility, but the configuration format is now Lua, and the .conf has an expiry date. If you’re starting today, there’s no point learning a format that’s on its way out: start directly in Lua.
The transition is well handled: Hyprland reads ~/.config/hypr/hyprland.lua if it exists, and falls back to the classic hyprland.conf otherwise. That gives you a free safety net: while you migrate (or while you learn), keep a working .conf in the directory, and if your Lua ever breaks, rename the .lua and you’re back on legacy instantly.
The package itself ships two files that are worth gold:
/usr/share/hypr/hyprland.lua # official example config
/usr/share/hypr/stubs/hl.meta.lua # stubs for the whole API (for the LSP)
The first is the starting point for your config. The second, if you set up your editor with a Lua language server, gives you autocompletion and documentation for the whole API while you write your config — something hyprlang could never offer.
I won’t paste my entire configuration here (it lives in my dotfiles ), but here’s the structure and the ideas that matter:
-- ~/.config/hypr/hyprland.lua
---@module 'hl.meta'
-- your programs, defined once
local terminal = "foot"
local menu = "rofi -show drun"
-- monitors
hl.monitor({
output = "", -- "" = any output
mode = "highres",
position = "auto",
scale = 1.875,
})
-- keyboard
hl.config({
input = { kb_layout = "es" },
})
-- autostart: hyprland.start replaces exec-once and only
-- fires when the compositor starts, not on every config reload
hl.on("hyprland.start", function()
hl.exec_cmd("waybar")
hl.exec_cmd("hyprpaper")
hl.exec_cmd("dunst")
end)
-- binds: a readable string, a dispatcher, and a description
-- that later shows up in `hyprctl binds` (a free cheat sheet)
local mainMod = "SUPER"
hl.bind(mainMod .. " + Return", hl.dsp.exec_cmd(terminal), { description = "Terminal" })
hl.bind(mainMod .. " + space", hl.dsp.exec_cmd(menu), { description = "Launcher" })
hl.bind(mainMod .. " + Q", hl.dsp.window.close(), { description = "Close window" })
-- workspaces: where you used to copy 20 nearly identical
-- lines, you now write a loop
for i = 1, 10 do
local key = i % 10
hl.bind(mainMod .. " + " .. key, hl.dsp.focus({ workspace = i }))
hl.bind(mainMod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = i }))
end
That final loop sums up why I like the change: the config stops being repeated text and becomes a tiny program. Binds can run Lua functions (my ALT+Tab dispatches two actions in a row, which used to be two binds “stacked” on the same key), and you can load things dynamically, like I do with the pywal colors.
Two pieces of advice learned the hard way:
Program defensively. A Lua error while loading the config throws Hyprland into “emergency mode” and you’re left with no keybinds at all. Anything you load from outside (files generated by other tools, APIs that may change) — wrap it in pcall and have a fallback. My config loads the pywal colors that way: if the file doesn’t exist or is half-written, fixed colors kick in and the session starts anyway.
Verify which config is actually being read. If you migrate from a .conf, remember the running session keeps the config it started with. I spent a whole day convinced my Lua was active while Hyprland was still running the old .conf. The check is one line:
hyprctl systeminfo | grep configProvider
# configProvider: lua <- what you want to see
the desktop, piece by piece
Hyprland only manages windows. Everything else you associate with “a desktop” is pieces you choose. This is the part where you shouldn’t copy me: for each need I tell you what I use and what sensible alternatives exist, and you decide.
Status bar: waybar
. Not much of a path to choose here: there’s no native Hyprland bar (careful, the hyprbars plugin is per-window title bars, not a status bar) and waybar is the de facto standard. It’s configured with JSON and CSS, and can be as sober or as busy as you want.
Application launcher: rofi
. Native Wayland support since version 2.0. I use it to launch apps, as a calculator, to search files and as the clipboard-history menu: four needs, one tool. Alternatives: wofi (simpler, it’s where I came from), fuzzel (minimalist). I tried hyprlauncher, the native option, and discarded it: it doesn’t search files and its dmenu mode didn’t work for my clipboard flow.
Notifications: dunst
. Lightweight, configured with a text file, never causes trouble. There’s no native Hyprland equivalent. Alternatives: mako, or swaync if you want a full notification center.
Wallpaper: hyprpaper + pywal
. hyprpaper is the native option and does one thing well. pywal is the magic trick: it extracts a color palette from your wallpaper and generates it for the rest of your apps (my terminal, waybar and Hyprland’s window borders share the wallpaper’s palette). Alternative: swww if you want animated wallpaper transitions.
Locking and idle: swaylock-effects + swayidle. The veteran combo: swayidle watches for inactivity and triggers the lock, swaylock draws the lock screen. The current native path is hyprlock + hypridle, and it’s probably what I’d recommend if you’re starting from zero; I haven’t migrated yet because what I have works and I’m doing that migration in phases.
Screenshots: hyprshot
+ hyprpicker. Here I have already moved to the native tools: hyprshot to capture a region, window or screen, and hyprpicker as a color picker that copies the hex value. Underneath they still use grim + slurp, which are worth keeping installed because other programs use them.
Clipboard with history: cliphist + wl-clipboard. A daemon stores everything you copy and a bind brings it up in rofi to recover anything. One of those pieces that, once tried, you can’t live without.
Night light: gammastep. Blue light filter based on time of day. The native path is hyprsunset; it’s on my pending-migrations list.
Audio: pipewire + wireplumber. We already installed it before rebooting. For daily use: pactl in the volume binds, playerctl to control music from the keyboard and pavucontrol when I need a graphical mixing desk.
File manager: thunar. Graphical, light, no pretensions. In the terminal, yazi or ranger are very worthy paths if you live in the console.
GTK looks: nwg-look. GTK apps don’t inherit your theme by magic on Wayland; nwg-look lets you pick theme, icons and cursor without fighting config files.
sudo pacman -S waybar rofi dunst hyprpaper python-pywal swaylock-effects swayidle \
hyprshot hyprpicker cliphist wl-clipboard gammastep pavucontrol playerctl \
thunar nwg-look
details of my setup that might save you hours
These are specific things in my configuration that don’t show up in generic guides and cost me real time:
Fractional scaling and GTK apps. My monitor runs at 1.875 scale, and with fractional scales GTK apps can misbehave (in my case, waybar stopped responding to clicks because gtk-layer-shell misaligned the coordinates). The fix was launching waybar with env GDK_SCALE=1 GDK_DPI_SCALE=1.875, forcing GTK not to scale and compensating the visual size. If you use integer scaling (1x, 2x), forget this paragraph.
External monitor brightness from the keyboard. Laptops ship brightnessctl and that’s it, but on a desktop the monitor’s brightness is changed over DDC/CI with ddcutil
. My brightness keys call a script that writes absolute values instead of relative ones, because my monitor doesn’t return reliable DDC readings and the relative form (which needs to read before writing) failed randomly. If your brightness binds “sometimes work”, this may be what’s happening to you.
Dotfiles in a repo. All of this configuration lives in github.com/zft9xgy/dotfiles
and syncs across my machines. The day this guide is truly needed, most of the work will be a git clone.
what about the homelab?
The previous version of this guide promised an “installing homelab” section that never arrived, and it’s not going to: the homelab deserves its own posts. If that path interests you, start with the beginning of the project (Spanish) and continue with Pi-hole in Docker with a macvlan network .
sources and resources
- Hyprland wiki : the reference. The configuration section already documents the Lua API.
- Installation guide - ArchWiki
- My dotfiles : the complete, real configuration of everything in this post.
And that’s it. Two years later, this guide has finally dropped the “[incomplete]” from its title. If you build your desktop with it and choose different paths from mine, I’d love to hear which ones: my contact details are below.