Hyprland Setup Guide

11 Aug 2026 • 9 min read

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 runs bspc config commands — config-as-code. Hyprland’s 0.55 move to Lua pushes it the same direction: your config is now real, executable code, not static key = value lines.
  • 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)

BASH
sudo pacman -Syu
sudo pacman -S hyprland kitty waybar wofi xdg-desktop-portal-hyprland \
                hyprpolkitagent qt5-wayland qt6-wayland mako swaybg

Fedora

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.

BASH
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-Xwayland

Ubuntu

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:

BASH
sudo add-apt-repository ppa:hyprwm/hyprland
sudo apt update
sudo apt install hyprland kitty waybar wofi \
                  hyprpolkitagent xdg-desktop-portal-hyprland

Note: polkit-kde-agent works, but hyprpolkitagent is 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

BASH
# From a display manager: select "Hyprland" as your session and log in
# From a bare TTY (Ctrl+Alt+F2):
Hyprland

Hyprland 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:

LUA
-- 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:

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

TEXT
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:

  1. Error isolation. Each require()d file runs in its own scope. A typo in binds.lua aborts that file and pops an error — it doesn’t take down monitors.lua or anything else. A single flat file gets no such protection.
  2. One concern per file — you can jump straight to cursor.lua or input.lua instead of scrolling one giant config, and git diffs stay small and reviewable.
  3. scheme/ and themes/ stay separate from settings/ — swap a color scheme or theme without touching binds or window rules.
  4. scripts/ stays out of the config namespace — shell/Lua helpers you exec at runtime are a different kind of artifact than declarative settings, and belong in their own directory.
  5. hyprland.lua is a clean entry point — it should do little beyond require() everything in a sensible order:
LUA
-- 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:

TEXT
/usr/share/hypr/hyprland.lua

That’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:

BASH
mkdir -p ~/.config/hypr
cp /usr/share/hypr/hyprland.lua ~/.config/hypr/hyprland.lua

The 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 FEELgeneral + resize_on_border/allow_tearing/layout fields settings/layout.lua These set the overall tiling behavior, gaps, and border size.
LOOK AND FEELdecoration block (rounding, shadow, blur, opacity) settings/decoration.lua Matches the file you already have.
LOOK AND FEELanimations 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

BASH
hyprctl reload     # reload config after saving
hyprctl            # inspect live state; also exposes a Lua REPL for testing hl.* calls

Sources

  • Hyprland Wiki — Configuring/Start, Installation, hyprpolkitagent
  • Hyprland news: “Lua-ification of Hyprland configs”
  • Wikipedia: Hyprland

Start searching

Enter keywords to search articles.