Location-aware Hyprland setup with automatic dock/undock detection.
One laptop, N layouts you name yourself (home / work / single by default):
- 3 external monitors + laptop off
- 2 external monitors + laptop display
- laptop only (no dock)
The right config is applied automatically when you plug or unplug the dock. No manual switching. The interactive installer (./install.sh) detects your current monitors and generates the per-layout config files from live hardware.
This started as the stock CachyOS Hyprland config (from the cachyos-hyprland-settings package, installed at /etc/skel/.config/hypr/). The location-aware monitor handling, screenshot script, hyprlock/hypridle integration, and a handful of keybind/app changes are mine; everything else follows CachyOS upstream.
If you're on CachyOS and want to compare against the stock config, diff -ru /etc/skel/.config/hypr/ ~/.config/hypr/ after install will show you exactly what changed.
Defined in hypr/config/defaults.conf. Swap to your own preferences there.
| Variable | I use | CachyOS upstream |
|---|---|---|
$terminal |
ghostty |
alacritty |
$browser |
zen-browser |
(not set) |
$filemanager |
nemo |
(empty) |
$applauncher |
rofi -show combi -modi window,run,combi -combi-modi window,run |
wofi |
$idlehandler |
hypridle |
swayidle |
$capturing |
bash ~/.config/hypr/scripts/screenshot_swappy |
three vars ($shot-region/$shot-window/$shot-screen) using grimblast |
Only the bindings that differ from CachyOS upstream. Everything else (workspace switching, focus movement, volume / brightness / playback, scratchpads, window grouping, gaps toggle) is unchanged. Full set in hypr/config/keybinds.conf.
| Shortcut | Action | Difference from CachyOS |
|---|---|---|
Super + Shift + Return |
Open floating terminal (termfloat) |
new |
Super + W |
Open browser | new |
Super + R |
Application launcher | moved from Super + Space |
Super + Shift + Space |
Toggle floating mode | moved from Super + V |
Super + F |
Maximize window | upstream toggled true fullscreen on this binding |
Super + Shift + F |
Toggle true fullscreen | new |
Super + Shift + R |
Resize submap | moved from Super + R (freed for launcher) |
Super + Shift + Q |
Exit Hyprland session | replaces Super + Shift + M (loginctl terminate-user) |
Super + Shift + S |
Screenshot to clipboard via swappy | replaces Print / Ctrl+Print / Alt+Print |
Super + Shift + P |
Open calculator (gnome-calculator) |
new |
Super + L |
Lock screen (hyprlock) |
replaces swaylock-fancy (incompatible with Hyprland 0.40+) |
+-----------------------+
| Hyprland IPC socket |
| (monitoradded / |
| monitorremoved) |
+----------+------------+
|
v
+-----------------------+
| monitor-listener | <- autostart, tails .socket2.sock
+----------+------------+
|
v
+-----------------------+
| location-switch | <- sources config/layouts.sh,
| | calls detect_location() with
| | hyprctl monitors -j; matches
| | EDID markers per layout.
+----------+------------+
|
v
+-----------------------+
| flip symlinks: |
| monitors.conf -> monitors.<loc>.conf
| workspaces.conf -> workspaces.<loc>.conf
| hyprctl reload |
+-----------------------+
hyprland.conf does source = config/monitors.conf and source = config/workspaces.conf. The real files are monitors.<layout>.conf - monitors.single.conf ships in the repo as a fallback; the wizard generates monitors.home.conf, monitors.work.conf, etc. for whatever layouts you name (and likewise for workspaces). location-switch just retargets the symlink and calls hyprctl reload. No config rewriting, no templating.
DisplayPort numbers in Wayland are assigned by detection order, which shifts across reboots, dock cycles, or even cable order. EDID descriptions (Dell Inc. DELL G2725D <serial>) are stable.
scripts/configure emits desc:<EDID> matchers automatically whenever the detected monitor reports a non-empty description. Connector names (DP-N, HDMI-A-N) are only used as a fallback for outputs without an EDID description string.
git clone <this-repo> hyprland-config
cd hyprland-config
./install.shOr, without cloning (always review a curl | bash before running it):
curl -fsSL https://raw.githubusercontent.com/iuliandita/hyprland-config/main/install.sh | bashinstall.sh copies everything to ~/.config/hypr/ and, when Hyprland is running and a TTY is attached, launches an interactive wizard (scripts/configure) that:
- lists detected monitors with resolution, refresh rate, and EDID description
- asks how many layouts you want (defaults: 1/2/3 based on detected monitors)
- asks layout names (defaults:
home,work,single) - captures the current layout from live hardware and stubs the rest for later
- proposes a workspace split per layout (
1 -> 10,2 -> 5,5,3 -> 3,4,3,4 -> 2,3,3,2) - asks for a unique EDID marker per layout so
location-switchcan auto-detect it
If there's no TTY or Hyprland isn't running (e.g. curl | bash from a plain shell), it skips the wizard and seeds a safe single fallback. Re-run ~/.config/hypr/scripts/configure later with each dock state plugged in to fill the stubs.
Flags:
./install.sh --wizard- force the wizard (errors if prerequisites are missing)./install.sh --no-wizard- skip the wizard (old behaviour: seedsinglesymlinks only)./install.sh --skip-deps- skip the dependency check (useful for CI / trusted environments)
Before touching any files, install.sh checks for the tools this config needs and groups them into tiers:
| Tier | Tools | If missing |
|---|---|---|
| Required | hyprland |
hard fail with a distro-specific install hint |
| Core | jq, notify-send (libnotify), ncat, hypridle, hyprlock |
warn; prompt to install |
| UX | waybar, mako, swaybg, rofi, grim, slurp, swappy |
warn; prompt to install |
| Defaults | ghostty, nemo, zen-browser, nwg-drawer, nwg-dock-hyprland, wob |
mention only; swap them in config/defaults.conf if you prefer your own |
Distro detection reads /etc/os-release (ID + ID_LIKE) and recognises these families:
- Arch: Arch, CachyOS, EndeavourOS, Manjaro, Garuda, Artix
- Debian: Debian, Ubuntu, Pop!_OS, Mint, elementary, Zorin, Kali, Raspbian
- Fedora: Fedora, Nobara, Bazzite, Silverblue, Kinoite, RHEL, CentOS
When core or UX tools are missing, the installer prints the exact install command for the detected family and offers four choices:
Install the repo-available ones now? [A]uto / [M]anual-wait / [S]kip / [Q]uit [S]:
- A (auto): runs the install command. Only wired up for Arch (package names are reliable across its derivatives). On Debian/Fedora this falls through to manual because names drift across versions and several tools aren't in default repos.
- M (manual-wait): pauses so you can install the tools in another shell, then press enter to continue.
- S (skip): continues with the install; fix it later.
- Q (quit): exits before writing any files.
Package names are mapped per family where they differ (e.g. notify-send -> libnotify on Arch/Fedora but libnotify-bin on Debian; ncat -> nmap on Arch, nmap-ncat on Fedora). Tools that aren't in a family's default repos (e.g. Hyprland itself on older Debian/Fedora, nwg-* on any non-Arch, ghostty/zen-browser everywhere) are listed separately with "build manually / enable AUR / COPR / PPA" guidance rather than silently omitted.
If there's no TTY (e.g. curl | bash from a non-graphical shell) the installer prints the commands and continues without prompting.
To enable NetworkManager-triggered switching (optional - monitor-listener alone works for dock hotplug):
sudo install -m 755 hypr/scripts/99-hypr-location /etc/NetworkManager/dispatcher.d/The dispatcher resolves the logged-in user's home directory via getent passwd, so no per-host editing is needed.
Re-run the wizard any time you want to regenerate these:
~/.config/hypr/scripts/configureIt rewrites config/monitors.<layout>.conf, config/workspaces.<layout>.conf, and config/layouts.sh (the detection function sourced by location-switch). Stub layouts for setups you can't plug in right now get TODO_external_N placeholders in their monitor config; when you dock to that setup, re-run the wizard and pick the matching layout number as "current" to auto-fill it.
If you'd rather hand-edit:
config/monitors.<layout>.conf- onemonitor=line per outputconfig/workspaces.<layout>.conf-workspace=N,monitor:desc:<EDID>per slotconfig/layouts.sh-detect_location()bash function; getshyprctl monitors all -joutput as$1, echoes a layout name
Prefer desc:<EDID description> over DP-N port names. DisplayPort numbers shuffle across reboots, dock cycles, and cable order; EDID descriptions are stable. Find them with:
hyprctl monitors all -j | jq -r '.[] | "\(.name)\t\(.description)"'config/defaults.conf- default apps ($terminal, $browser, etc.)config/keybinds.conf- all keyboard shortcutsconfig/colors.conf- CachyOS palette variablesconfig/autostart.conf- things launched at login (waybar, mako, monitor-listener, ...)
hypr/
├── hyprland.conf # Entry point; sources everything in config/
├── hypridle.conf # Idle -> lock / DPMS off
├── hyprlock.conf # Lock screen (Tokyo Night)
├── config/
│ ├── animations.conf
│ ├── autostart.conf
│ ├── colors.conf
│ ├── decorations.conf
│ ├── defaults.conf
│ ├── environment.conf
│ ├── input.conf
│ ├── keybinds.conf
│ ├── layouts.sh # detect_location() sourced by location-switch (regenerated by wizard)
│ ├── monitor.conf # Fallback catch-all (unused by default)
│ ├── monitors.single.conf # Laptop-only default (wizard creates monitors.<layout>.conf)
│ ├── variables.conf
│ ├── windowrules.conf
│ └── workspaces.single.conf # Wizard creates workspaces.<layout>.conf for other layouts
└── scripts/
├── configure # Interactive layout wizard
├── location-switch # Detect + flip symlinks + reload
├── monitor-listener # Hyprland IPC -> location-switch
├── 99-hypr-location # NetworkManager dispatcher (optional)
├── screenshot # grim + slurp
├── screenshot_area
├── screenshot_full
└── screenshot_swappy
monitors.conf and workspaces.conf are runtime-generated symlinks. They are not in the repo; install.sh creates them on first run.
install.sh checks these for you and prints the exact install command for your distro family. The list, for reference:
- Required:
hyprland0.40+ (needsext-session-lock-v1) - Core:
jq,notify-send(libnotify),ncat(fromnmapon Arch,nmap-ncaton Fedora),hypridle,hyprlock - UX:
waybar,mako,swaybg,rofi,grim,slurp,swappy - Default apps (swap in
config/defaults.conf):ghostty,nemo,zen-browser,nwg-drawer,nwg-dock-hyprland,wob
Integration tests live in tests/wizard_test.sh. They mock hyprctl/pgrep, redirect XDG_CONFIG_HOME to a scratch tree, drive the wizard via a PTY, and assert on the generated files. CI runs them plus shellcheck, shfmt, actionlint, trivy, and gitleaks on every push/PR.
Run locally:
./tests/wizard_test.shNo real Hyprland required - everything is mocked. Pass --skip-deps to install.sh when testing manually so the dep check doesn't prompt.
- Windows stay with their workspace across dock transitions. Unplug -> all 10 workspaces land on the laptop; replug -> workspaces re-home to their assigned monitors. Tiled layouts re-tile automatically. Floating windows keep absolute coords and may land oddly; nudge them.
swaylock/swaylock-effectsis incompatible with Hyprland 0.40+ (noext-session-lock-v1). Stick withhyprlock.- The
location-switchidempotency guard compares symlink targets, so repeated firing while already in the right state is a no-op.