Hyprland Setup Guide
Install Hyprland on Arch, Fedora, or Ubuntu, understand how it compares to bspwm, and lay out a modular Lua config.
A step-by-step guide to installing Hyprland (Arch, Fedora, Ubuntu), understanding where it came from, how it compares to bspwm, and how to lay out a modular Lua config.
Step 1 — Know what you’re installing
Hyprland is a Wayland compositor (not just a window manager — it is the display server layer) written in C++ by Vaxry, first released in 2022. It became one of the most talked-about Linux setups because it combined a keyboard-driven tiling workflow with animations, blur, and rounded corners built in natively, rather than bolted on. It regularly ranks near the top of community surveys (e.g. the Arch Linux survey), it’s the default compositor of the Omarchy distro, and it was sponsored by Framework Computer in 2025 — though that sponsorship drew some criticism over community-conduct concerns.
As of Hyprland 0.55 (May 2026), the old hyprland.conf config language (“hyprlang”) was replaced by a full Lua scripting API. That’s why your config is .lua files, not .conf.
How it relates to bspwm (not a fork, but a real conceptual cousin):
- Hyprland’s default layout, dwindle, recursively splits space in two for each new window — the same binary-space-partitioning model bspwm is built entirely around.
- Both follow a “do tiling well, bring your own bar/launcher/notifications” philosophy — bspwm delegates keys to
sxhkd; Hyprland expects you to pair it with Waybar, wofi/rofi, and a notification daemon. - bspwm’s config is a shell script (
bspwmrc) that runsbspc configcommands — config-as-code. Hyprland’s 0.55 move to Lua pushes it the same direction: your config is now real, executable code, not statickey = valuelines. - Where they differ: bspwm is X11-only and needs a separate compositor (e.g.
picom) for effects. Hyprland is Wayland-only and is the compositor — animations and blur are native.
Step 2 — Install Hyprland on your distro
Pick your distro below.
Arch Linux (officially supported)
sudo pacman -Syu
sudo pacman -S hyprland kitty waybar wofi xdg-desktop-portal-hyprland \
hyprpolkitagent qt5-wayland qt6-wayland mako swaybgFedora
Fedora 39+ ships Hyprland in official repos, but the community COPRs (solopasha/hyprland or ashbuk/Hyprland-Fedora) track new releases faster — worth enabling if dnf info hyprland shows a stale version.
sudo dnf copr enable solopasha/hyprland
sudo dnf install hyprland kitty waybar wofi \
pipewire pipewire-pulseaudio wireplumber \
hyprpolkitagent xdg-desktop-portal-hyprland \
grim slurp wl-clipboard xorg-x11-server-XwaylandUbuntu
Ubuntu’s own repo package is old and not recommended by the Hyprland wiki itself. Use a maintained PPA instead (check which one currently tracks your release, e.g. ppa:hyprwm/hyprland or a community PPA), or a scripted installer like JaKooLit’s Ubuntu-Hyprland:
sudo add-apt-repository ppa:hyprwm/hyprland
sudo apt update
sudo apt install hyprland kitty waybar wofi \
hyprpolkitagent xdg-desktop-portal-hyprlandNote:
polkit-kde-agentworks, buthyprpolkitagentis the Hyprland-native polkit agent (QT/QML, built for this ecosystem) — use it instead unless it’s unavailable in your repos, in which case fall back to KDE’s agent.
Step 3 — First launch
# From a display manager: select "Hyprland" as your session and log in
# From a bare TTY (Ctrl+Alt+F2):
HyprlandHyprland auto-generates ~/.config/hypr/hyprland.lua on first run if none exists. Since Lua is a full scripting language, only pull in require()d modules or example configs you actually trust — Lua can run arbitrary code on your machine.
Step 4 — Start the polkit agent
Add this to your autostart so GUI apps can prompt for elevated privileges:
-- autostart.lua
hl.exec("systemctl --user start hyprpolkitagent")(If you’re using uwsm: systemctl --user enable --now hyprpolkitagent.service instead.)
Step 5 — Set your environment variables (the most important step)
This is the part that actually determines whether GTK, Qt, Electron, and SDL apps render correctly under Wayland instead of falling back to a blurry XWayland session. Put this in settings/env.lua:
-- settings/env.lua
hl.env("QT_QPA_PLATFORM", "wayland")
hl.env("ELECTRON_OZONE_PLATFORM_HINT", "auto")
hl.env("GDK_BACKEND", "wayland,x11")
hl.env("SDL_VIDEODRIVER", "wayland")
hl.env("CLUTTER_BACKEND", "wayland")What each one does:
| Variable | Why it matters |
|---|---|
QT_QPA_PLATFORM=wayland |
Forces Qt apps (Dolphin, OBS’s Qt UI, etc.) to use the Wayland backend instead of falling back to XWayland. |
ELECTRON_OZONE_PLATFORM_HINT=auto |
Lets Electron apps (VS Code, Discord, Slack) auto-detect Wayland via Ozone and use native Wayland rendering when available, without hardcoding one platform. |
GDK_BACKEND=wayland,x11 |
GTK apps try Wayland first, and fall back to X11 if a specific app can’t do Wayland — the comma-separated order matters. |
SDL_VIDEODRIVER=wayland |
SDL2 games/apps render natively on Wayland instead of going through XWayland. |
CLUTTER_BACKEND=wayland |
Clutter-based apps (some GNOME components) use the Wayland backend directly. |
Get this wrong (or skip it) and you’ll still have a working desktop, but a chunk of your apps will silently render through XWayland — meaning no proper HiDPI scaling, worse input latency, and screen-tearing on some of them. This is genuinely more impactful to day-to-day experience than any animation or theme setting.
Step 6 — Lay your config out like this
hypr/
├── autostart.lua
├── hyprland.lua # entry point — only require()s
├── scheme/
│ └── current.lua
├── scripts/
├── settings/
│ ├── animations.lua
│ ├── binds.lua
│ ├── cursor.lua
│ ├── decoration.lua
│ ├── env.lua # step 5 lives here
│ ├── functions.lua
│ ├── gestures.lua
│ ├── input.lua
│ ├── layout.lua
│ ├── misc.lua
│ ├── monitors.lua
│ ├── rules.lua
│ ├── variables.lua
│ └── workspaces.lua
└── themes/Why this matters specifically under the Lua config system:
- Error isolation. Each
require()d file runs in its own scope. A typo inbinds.luaaborts that file and pops an error — it doesn’t take downmonitors.luaor anything else. A single flat file gets no such protection. - One concern per file — you can jump straight to
cursor.luaorinput.luainstead of scrolling one giant config, and git diffs stay small and reviewable. scheme/andthemes/stay separate fromsettings/— swap a color scheme or theme without touching binds or window rules.scripts/stays out of the config namespace — shell/Lua helpers youexecat runtime are a different kind of artifact than declarative settings, and belong in their own directory.hyprland.luais a clean entry point — it should do little beyondrequire()everything in a sensible order:
-- hyprland.lua
require("settings.env")
require("settings.monitors")
require("settings.input")
require("settings.gestures")
require("settings.cursor")
require("settings.layout")
require("settings.decoration")
require("settings.animations")
require("settings.binds")
require("settings.rules")
require("settings.workspaces")
require("settings.misc")
require("scheme.current")
require("autostart")Housekeeping: settings/rules.lua.bk from your original tree is a stray backup sitting inside a directory Hyprland otherwise treats as pure config. Nothing require()s it, so it’s harmless, but move backups into your dotfiles’ git history (or a backups/ folder outside hypr/) instead of leaving .bk files next to live config.
Step 7 — Splitting the default hyprland.lua into your file tree
On Arch, once Hyprland is installed, the full example/skeleton config ships at:
/usr/share/hypr/hyprland.luaThat’s the same file as the one in the upstream repo’s example/ directory — a single ~350-line file with everything in it, and it even tells you in a comment near the top: “You can (and should!!) split this configuration into multiple files.” Copy it out first before editing anything:
mkdir -p ~/.config/hypr
cp /usr/share/hypr/hyprland.lua ~/.config/hypr/hyprland.luaThe skeleton is laid out in clearly commented sections (---- MONITORS ----, ---- LOOK AND FEEL ----, ---- KEYBINDINGS ----, etc.). Here’s how each section maps onto your settings/ tree — copy the block, paste it into the matching file, and delete it from hyprland.lua:
| Skeleton section | Goes in | Notes |
|---|---|---|
MONITORS (hl.monitor({...})) |
settings/monitors.lua |
One hl.monitor() call per display. |
MY PROGRAMS (local terminal = "kitty" etc.) |
settings/variables.lua |
These are plain Lua locals — if other files need them, return a table instead of local, so it can be pulled in via local vars = require("settings.variables"). |
AUTOSTART (hl.on("hyprland.start", ...)) |
autostart.lua |
Keep this at the root, as in your tree — it’s a distinct concern from compositor behavior. |
ENVIRONMENT VARIABLES (hl.env(...)) |
settings/env.lua |
This is where the Wayland vars from Step 5 belong too. |
PERMISSIONS (hl.permission(...)) |
settings/misc.lua |
Small and infrequently touched; not worth its own file unless it grows. |
LOOK AND FEEL → general + resize_on_border/allow_tearing/layout fields |
settings/layout.lua |
These set the overall tiling behavior, gaps, and border size. |
LOOK AND FEEL → decoration block (rounding, shadow, blur, opacity) |
settings/decoration.lua |
Matches the file you already have. |
LOOK AND FEEL → animations toggle, hl.curve(...), hl.animation(...) calls |
settings/animations.lua |
Keep curves and animation leaves together — they reference each other by name. |
dwindle / master / scrolling layout configs |
settings/layout.lua |
Same file as general layout — it’s all “how tiling behaves.” |
MISC (hl.config({ misc = {...} })) |
settings/misc.lua |
|
INPUT (kb_layout, sensitivity, touchpad, hl.device(...)) |
settings/input.lua |
|
hl.gesture({...}) calls |
settings/gestures.lua |
Split out from INPUT even though the skeleton keeps them together — you already separate these, and it keeps touch/trackpad gestures independently reviewable from keyboard/mouse input. |
KEYBINDINGS (mainMod, all hl.bind(...)) |
settings/binds.lua |
This section is usually the longest in the skeleton — it fully justifies its own file. |
WINDOWS AND WORKSPACES (hl.window_rule(...), hl.layer_rule(...), workspace rules) |
settings/rules.lua |
|
Reusable helper functions you write yourself (e.g. wrapping repeated hl.window_rule patterns) |
settings/functions.lua |
Not present in the stock skeleton — this is your own abstraction layer, require()d by whichever files call into it. |
| Colors / palette values | scheme/current.lua |
Also not in the stock skeleton — pull hardcoded hex colors like "rgba(33ccffee)" out of decoration.lua/layout.lua and reference them from here instead, so a theme swap only touches one file. |
Once everything is moved out, hyprland.lua itself should shrink down to just the require() chain shown in Step 6 — nothing else needs to remain in the root file.
A word of caution when splitting: a few skeleton sections define local Lua variables that later sections reference (terminal, fileManager, menu, mainMod). Once these live in separate files, local won’t cross the require() boundary — each require() runs in its own scope, by design (that’s what gives you the error isolation from Step 6). Have settings/variables.lua return a table of these values, and have settings/binds.lua do local vars = require("settings.variables") at the top and reference vars.terminal instead of a bare local.
Step 8 — Reload and debug
hyprctl reload # reload config after saving
hyprctl # inspect live state; also exposes a Lua REPL for testing hl.* callsSources
- Hyprland Wiki — Configuring/Start, Installation, hyprpolkitagent
- Hyprland news: “Lua-ification of Hyprland configs”
- Wikipedia: Hyprland