CachyOS Hyprland Setup
Configuration applied to a CachyOS install running Hyprland + HyDE.
Started: 2026-05-15 · Rewritten for the Lua config: 2026-07-29
The one big gotcha: HyDE migrated to Lua, and it silently killed every .conf
On 2026-07-29 a HyDE update switched Hyprland to a Lua configuration. The log confirms it:
[cfg] Config is lua, loading lua mgr
Hyprland now loads ~/.local/share/hypr/hyde.lua. The entire .conf chain is no longer sourced by anything — including ~/.config/hypr/workflows/default.conf, which the previous version of this doc called "the reliable override file."
Nothing warned about this. The files still sit there looking authoritative. Everything in them had quietly stopped applying:
| Setting | .conf said | Runtime actually was |
|---|---|---|
| DP-1 | 2560x1440@240, scale 1.333 | 2560x1440**@60**, scale 1.0 |
| Window opacity | active 1.0 / inactive 0.9 | 0.9 / 0.75 (HyDE stock) |
| Workspaces | 3 / 4 / 3 per monitor | unassigned |
Super+D darken-monitors | bound | gone |
Super+M media-toggle | bound | gone |
Lesson: after any HyDE update, verify at runtime — never trust the config file. hyprctl monitors, hyprctl binds, hyprctl getoption. See Verification.
The dead files now carry a warning header so they can't fool anyone again. They were left in place rather than deleted, in case HyDE ever reverts.
The new load order
~/.local/share/hypr/hyde.lua requires, in order:
variables → defaults → window_rules → layer_rules → env → dynamic
→ key_binds → events → start_up
→ check_require("hyprland") ← ~/.config/hypr/hyprland.lua (US)
→ check_require("lua_state.workflows") ← only thing after us
~/.config/hypr/hyprland.lua is the override point. It loads after every HyDE default, and in Lua "last declaration wins" — so anything redefined there beats HyDE.
hyde.lua adds ~/.config/hypr/?.lua to package.path, so require("monitors") picks up ~/.config/hypr/monitors.lua. That path is the only config dir exposed to Lua — which is why Hyprland overrides must live in ~/.config/hypr/, even though HyDE's own settings belong in ~/.config/hyde/config.toml.
Second gotcha: the IPC command language changed too
The Lua switch didn't just move config files — it changed what Hyprland's IPC socket accepts. Every legacy string command is now a Lua syntax error, and the failure is silent to whatever sent it:
$ hyprctl dispatch workspace 5
error: [string "return hl.dispatch(workspace 5)"]:1: ')' expected near '5'
→ Note: dispatch in lua is a shorthand for hl.dispatch(...), your syntax might need to be updated.
$ hyprctl keyword decoration:rounding 10
keyword can't work with non-legacy parsers. Use eval.
dispatch now evaluates Lua, and keyword is refused outright. workspace 5 isn't valid Lua at all (f 5 is a syntax error — Lua only allows paren-less calls with a string or table literal), so no shim is possible. There is no legacy-compat option in the schema and no legacy IPC endpoint.
Translation table:
| Old | New |
|---|---|
hyprctl dispatch workspace 5 | hyprctl dispatch "hl.dsp.focus({workspace = 5})" |
hyprctl dispatch workspace r+1 | hyprctl dispatch "hl.dsp.focus({workspace = 'r+1'})" |
hyprctl dispatch pin active | hl.dsp.window.pin({window = w}) |
hyprctl dispatch togglefloating active | hl.dsp.window.float({action = "toggle", window = w}) |
hyprctl keyword <opt> <val> | hl.config({...}) / hyprctl eval |
hyprctl keyword source foo.conf | require("...") the Lua equivalent |
This breaks third-party tools and HyDE's own scripts, which is where most of the damage below comes from. Known casualties fixed here: Waybar's workspace buttons, window.pin.sh (Super+Shift+F), and gamemode.sh.
Third gotcha: HyDE's Lua state files were never generated
dynamic.lua pulls the theme, UI, animations and layouts from generated Lua under ~/.local/state/hyde/lua_state/ — via check_require, which returns nil silently when the file is missing:
local theme_config = check_require("lua_state.hypr_theme") or {} -- silently {} if absent
After the update only colors.lua and shaders.lua existed. hypr_theme.lua, ui.lua, animations.lua and layouts.lua were all missing, so the theme and animation config simply never loaded and everything fell back to bare Hyprland defaults. Nothing logged a word. (hyde-shell validate does report related errors: "No Hyprland variables generated, skipping file write".)
Check with:
ls ~/.local/state/hyde/lua_state/ # want: colors, hypr_theme, ui, shaders
Where each kind of setting goes
| Setting | Home | Survives HyDE update? |
|---|---|---|
| Hyprland (monitors, binds, rules) | ~/.config/hypr/hyprland.lua | ✅ |
| HyDE knobs (theme, wallpaper, apps) | ~/.config/hyde/config.toml | ✅ |
| Waybar layout | ~/.config/waybar/layouts/12-custom.jsonc | ✅ |
| Waybar CSS | ~/.config/waybar/user-style.css | ✅ |
| Idle/lock | ~/.config/hypr/hypridle.conf | ⚠️ reset by the Lua migration |
| Kitty | ~/.config/kitty/kitty.conf | ✅ |
Anything ~/.config/hypr/*.conf | dead | n/a — not sourced |
Hyprland — ~/.config/hypr/hyprland.lua
Everything Hyprland-side lives in this one file.
Monitors
Goal: cursor crosses monitors at equal vertical levels; text on the 1440p panel matches the physical size of the 1080p panels. All three are ~27".
Fix: scale the 1440p panel by 4/3 so its logical resolution becomes 1920×1080.
nwg-displays already writes ~/.config/hypr/monitors.lua (and a matching .conf, now dead). Nothing required it, so:
require("monitors")
That file contains:
hl.monitor({ output = "HDMI-A-1", mode = "1920x1080@60.0", position = "0x0", scale = 1.0 })
hl.monitor({ output = "DP-1", mode = "2560x1440@240.0", position = "1920x0", scale = 1.33 })
hl.monitor({ output = "DP-2", mode = "1920x1080@60.0", position = "3840x0", scale = 1.0 })
Use nwg-displays to change these — it regenerates the file, and require picks it up.
Fractional scaling can blur text in some XWayland apps. HyDE sets
xwayland.force_zero_scaling = truein its defaults, which mitigates it. If it still bites, try scale 1.25.
Follow-up not applied: DP-2 → 165 Hz, HDMI-A-1 → 75 Hz.
Workspaces
10 workspaces split 3 / 4 / 3, declared directly in hyprland.lua rather than via require("workspaces"):
-- ipairs over an ORDERED list, never pairs() over a keyed table.
for _, w in ipairs({
{ 1, "HDMI-A-1", true }, { 2, "HDMI-A-1" }, { 3, "HDMI-A-1" },
{ 4, "DP-1", true }, { 5, "DP-1" }, { 6, "DP-1" }, { 10, "DP-1" },
{ 7, "DP-2", true }, { 8, "DP-2" }, { 9, "DP-2" },
}) do
hl.workspace_rule({
workspace = tostring(w[1]), monitor = w[2],
persistent = true, default = w[3] or nil,
})
end
⚠️ Use
ipairsover an ordered list, notpairsover a keyed table. Lua's
pairs()iterates in nondeterministic hash order, so the workspaces get
created out of order — DP-1 came out as 4, 10, 5, 6, and the bar rendered
them that way. Creation order also sticks:hyprctl reloaddoes not renumber
existing workspaces, so a fresh session is needed to see the corrected order at
the compositor level. (Waybar sorts by name anyway — see below — so the bar is
correct either way; this just keepshyprctl workspacessane.)
Two reasons not to use nwg-displays' workspaces.lua here:
- it only ever writes 1–9 (workspace 10 has to be added by hand every time), and
- it has no
persistent, which the Waybar bar now depends on (see Waybar below).
persistent = true keeps empty workspaces alive in the compositor so their buttons stay visible. Verify: hyprctl workspaces -j | jq '[.[].id] | sort' should list all 10 even with empty ones.
nwg-displays still owns the monitor layout (monitors.lua); only workspaces moved out of its control.
Animations
The animation config was another silent casualty — the old chain (animations.conf → animations/theme.conf) is dead and lua_state/animations.lua was never generated, so no preset loaded at all. Everything fell back to the bare global leaf at speed 8 = 0.8 s per transition, which is what made the desktop feel sluggish.
hyde.config.anim.duration_scale = 4 / 3 -- multiplies every speed in the preset
require("animations.fast") -- ~0.4s; other presets in lua/animations/
-- fast.lua leaves these untouched, so they'd keep the slow 0.8s global.
-- `layers` is the one that makes rofi/menus/notifications feel slow.
for _, leaf in ipairs({ "global", "layers", "layersIn", "layersOut",
"fadeGlow", "zoomFactor", "monitorAdded" }) do
hl.animation({ leaf = leaf, enabled = true, speed = 4, bezier = "md3_decel" })
end
duration_scale is the one knob to tune — 1.0 is the preset's own timing, 0.5 halves it, lower is snappier. It must be set before the require; the preset bakes it in at load time. For no animation at all, require("animations.00-disable"). Keep the explicit speed in the loop below roughly in step with it (they are raw speeds, so duration_scale does not apply to them).
Resulting speeds (Hyprland speed is in deciseconds, so 4 = 0.4 s):
| Leaf | Speed | |
|---|---|---|
| windows | 4.0 | 0.40 s |
| workspaces | 4.67 | 0.47 s |
| fade | 3.33 | 0.33 s |
| layers, global | 4.0 | 0.40 s |
fast.lua's base windows speed is 3, so duration_scale is just target / 3 — 4/3 for 0.4 s, 5/3 for 0.5 s, and so on.
Check with hyprctl animations. Anything showing overridden: 0 is inheriting global; the leftover bezier names (md3_decel, linear, …) are curves, not animations, and are meant to show that way.
Theme — regenerating hypr_theme.lua
The theme's Hyprland half lives in ~/.config/hypr/themes/theme.conf, which is dead, and its Lua replacement was never generated. Everything reverted to Hyprland defaults:
| Setting | Theme wants | Was getting |
|---|---|---|
rounding | 10 | 0 (square corners) |
gaps_in / gaps_out | 3 / 8 | 5 / 20 |
border_size | 2 | 1 |
blur size / passes | 6 / 3 | 8 / 1 |
resize_on_border | true | false ← no mouse edge-resize |
| border colors | Tokyo Night gradient | plain default |
resize_on_border = false is why windows couldn't be resized by dragging their edges.
Regenerate with HyDE's own command (from theme.switch.sh:143, using hyq from hyprquery-git):
hyq --dump "$HOME/.config/hyde/themes/$(. ~/.local/state/hyde/staterc; echo "$HYDE_THEME")/hypr.theme" \
--schema "$HOME/.local/share/hypr/schema/hyprland-lua.json" \
--export lua > ~/.local/state/hyde/lua_state/hypr_theme.lua
hyprctl reload
Because this is the file HyDE itself writes, switching themes normally will keep it current. Verify with hyprctl getoption general:resize_on_border → true.
Window opacity
Focused 100% / unfocused 90% globally; terminals keep HyDE's original look; browsers always fully opaque.
hl.config({
decoration = {
active_opacity = 1.0,
inactive_opacity = 0.9,
fullscreen_opacity = 1.0,
},
})
hl.window_rule({
name = "user_terminal_opacity",
match = { class = "^(kitty|Alacritty|foot|org\\.wezfurlong\\.wezterm|com\\.mitchellh\\.ghostty)$" },
opacity = "0.9 0.75 1.0",
})
hl.window_rule({
name = "user_browser_opacity",
match = { class = "^([Ff]irefox|brave-browser|[Cc]hromium|google-chrome|zen|librewolf|vivaldi.*)$" },
opacity = "1.0 1.0 1.0",
})
⚠️
opacitymust be a STRING —"0.9 0.75 1.0", space-separatedactive inactive fullscreen.
A Lua table{0.9, 0.75, 1.0}is rejected, and the rejection is invisible in the log.
It only appears inhyprctl configerrors:
hl.window_rule: field 'opacity': string type requires a stringThe old
.conf$&separator (opacity 0.9 $& 0.75 $& 1) was likewise broken. Plain floats, always.
Browsers are pinned opaque because text over a translucent background is the main readability offender.
Keybindings
HyDE's Lua defaults are a completely different scheme from the old .conf ones. These seven are declared in hyprland.lua; everything else is HyDE stock.
| Bind | Action | Note |
|---|---|---|
Super+Return | terminal | was unbound |
Super+Space | rofi app finder | was unbound; Super+A still works |
Super+T | toggle floating | overrides HyDE's terminal; Super+W also floats |
Super+F | cycle fullscreen | overrides HyDE's pin; Super+F11 also works |
Super+Shift+F | toggle pin | relocated off Super+F; reimplemented natively (see below) |
Super+D | darken-monitors | restored — killed by the migration |
Super+M | media-controller-toggle | restored; Super+Alt+M (mute) untouched |
Super+` | dropdown terminal | HyDE default, re-declared so an update can't drop it |
Super+Alt+G | game mode | reimplemented natively (see below) |
Ctrl+Delete | removed — see below | |
F12 / F11 | volume up / down | bare keys; restores what HyDE used to ship |
Volume on bare F11/F12
HyDE's own binddel = , F11/F12 lines sit commented out at key_binds.lua:181-183; only the XF86Audio* media keys are bound now. Restored with the same flags HyDE uses:
hl.bind("F12", hl.dsp.exec_cmd(hyde.sh.volumecontrol("-o", "i")),
{ description = "[Hardware Controls|Audio] increase volume", locked = true, repeating = true })
hl.bind("F11", hl.dsp.exec_cmd(hyde.sh.volumecontrol("-o", "d")),
{ description = "[Hardware Controls|Audio] decrease volume", locked = true, repeating = true })
repeating steps while held, locked keeps them live on the lock screen. Steps are 5%. The XF86Audio* keys stay bound alongside these.
⚠️ These are bare keys, so Hyprland swallows them before applications see them — browsers lose F11 fullscreen and F12 devtools. Super+F11 (cycle fullscreen) is a separate bind and is unaffected. Use Super+F11/Super+F12 instead if you'd rather keep F11/F12 with applications.
Note hyprctl binds does not surface the repeating flag (it shows repeat: None even for HyDE's own binds that set it), so don't read that as the flag having failed.
Removing a HyDE bind
hl.unbind(...) removes an inherited bind outright, rather than shadowing it with a no-op:
hl.unbind("CTRL + Delete")
HyDE binds Ctrl+Delete to exit hyprland session — it kills the session instantly with no confirmation, and it is far too easy to hit by accident for something unrecoverable. Ctrl+Alt+Delete (logout menu, which does prompt) is deliberately left in place.
Verify with hyprctl binds -j | jq '.[] | select(.key|ascii_downcase|contains("delete"))' — only the modmask: 12 (Ctrl+Alt) entry should remain.
Two of these had to be rewritten because the HyDE shell scripts behind them use the now-dead legacy IPC:
-
Pin —
hyde.sh.window.pin()runshyprctl dispatch pin active/togglefloating active. Replaced with a Lua function usinghl.dsp.window.pinandhl.dsp.window.float. A window must float to be pinned, so it floats on the way in and restores tiling on the way out. -
Game mode —
gamemode.shworks byhyprctl keyword source .../workflows/gaming.conf, andhyprctl keywordis refused entirely under the Lua parser. The old script therefore only ever touched its lock file, so game mode reported itself on while applying nothing. Now driven offrequire("workflows.gaming"), withhyprctl reloadto switch back off. The lock file path ($XDG_RUNTIME_DIR/hyde/gamemode.lck) is kept so any HyDE indicator stays in sync.Note
requirememoises, sopackage.loaded["workflows.gaming"] = nilis needed before re-requiring or a second activation is a silent no-op.The
locked = trueflag on this bind is HyDE's own (key_binds.lua:212) and simply means the bind still works while the session is locked — same as the brightness keys. It is not a fault.
Bind syntax:
local MOD = hyde.config.modifiers.main
local _apps = hyde.config.app
hl.bind(MOD .. " + Return", hl.dsp.exec_cmd(_apps.terminal),
{ description = "[Launcher|Apps] terminal emulator" })
hl.bind(MOD .. " + T", hl.dsp.window.float({ action = "toggle" }),
{ description = "[Window Management] toggle floating" })
hl.bind(MOD .. " + D", hl.dsp.exec_cmd(os.getenv("HOME") .. "/.local/bin/darken-monitors"),
{ description = "[Hardware Controls|Brightness] toggle monitor dim" })
Useful handles: hyde.sh.menu.apps(), .windows(), .clipboard(); hyde.sh.window.pin(); hyde.config.app.{terminal,browser,editor,explorer}.
Fullscreen needs its own function. HyDE's cycle_fullscreen is a file-local in key_binds.lua, so it isn't reachable — it's replicated verbatim in hyprland.lua:
local cycle_fullscreen = function()
local active_window = assert(hl.get_active_window(), "No active window to toggle fullscreen")
local current_state = tonumber(active_window.fullscreen) or 0
local next_state = (current_state + 1) % 3
hl.dispatch(hl.dsp.window.fullscreen_state({
internal = next_state, client = next_state, window = active_window,
}))
end
Apply everything with hyprctl reload.
Screen shaders — two rival implementations
HyDE ships both a legacy shaders.sh and a Lua shaders.lua in ~/.local/lib/hyde/, and they don't talk to each other:
| writes | result | |
|---|---|---|
shaders.sh (legacy) | ~/.config/hypr/shaders/.compiled.cache.glsl + ~/.config/hypr/shaders.conf | both dead — nothing reads either |
shaders.lua (current) | ~/.local/state/hyde/compiled.cache.glsl + lua_state/shaders.lua | what Hyprland actually reads |
hyde-shell resolves shaders to the .sh, so selecting a shader printed "compiled successfully" while changing nothing that Hyprland reads — the compiled cache Hyprland pointed at was never touched. Telltale: two cache files with different mtimes and md5s.
Fix — take the .sh out of the resolution path so hyde-shell shaders finds the Lua one:
mv ~/.local/lib/hyde/shaders.sh ~/.local/lib/hyde/shaders.sh.disabled-legacy
hyde-shell shaders --current # now the Lua selector
hyde-shell shaders --set blue-light-filter
hyde-shell shaders --select # rofi picker
The Lua path sets decoration:screen_shader live via hl.config — no reload needed. Setting disable writes an empty string, which unsets the shader rather than loading a passthrough.
Verify a switch really took by watching the live option change, not the script's output:
hyprctl getoption decoration:screen_shader
~/.local/lib/hyde/ is HyDE-managed, so an update may restore shaders.sh and silently reinstate the broken path. Re-check after updates. Note staterc's HYPR_SHADER and lua_state/shaders.lua can also disagree — the Lua state file is the one that counts.
Keyboard — CapsLock/Escape swap
Handled outside Hyprland now (keyd or equivalent), so there is no kb_options line in hyprland.lua and no localectl set-x11-keymap call. One layer, covering Hyprland, SDDM, X11 apps, and the TTY — which the old two-layer approach never did.
Idle / lock — ~/.config/hypr/hypridle.conf
This file is sourced directly (no Lua shadow), but the HyDE update reset it: DPMS dropped to 600 s and the lock listener vanished entirely. Restored cascade:
| Stage | Timeout |
|---|---|
| Dim to 1% | 600 s (10 min) |
loginctl lock-session | 1800 s (30 min) |
| DPMS off | 2100 s (35 min) |
systemctl suspend | 3600 s (60 min) |
Hypridle doesn't reload on SIGHUP — restart the unit:
systemctl --user restart hyde-Hyprland-idle.service
Verify: pgrep -c hypridle → 1, and the journal should log found 4 rules.
Kitty — ~/.config/kitty/kitty.conf
include hyde.conf at the top, overrides below — later wins, and HyDE never touches this file.
# was 25 -- reclaims a lot of screen
window_padding_width 4
# highlight = straight to clipboard
copy_on_select yes
⚠️ Kitty has no trailing inline comments.
window_padding_width 4 # was 25makes kitty parse the comment as part of the value and fail with "could not convert string to float". Comments go on their own line. (The previous version of this doc had exactly this bug in its snippet.)
Reload an open window with Ctrl+Shift+F5. Verify without launching a window:
kitty +runpy "from kitty.config import load_config; o=load_config('$HOME/.config/kitty/kitty.conf'); print(o.window_padding_width, o.copy_on_select)"
(kitty --debug-config does not exist in 0.48.1.)
Waybar
Goal: clock left, per-monitor workspaces center (HDMI-A-1: 1–3, DP-1: 4–6+10, DP-2: 7–9), idle_inhibitor in the right pill.
Unaffected by the Lua migration — this is Waybar's own config, not Hyprland's.
Durable approach: user layout file
Editing ~/.config/waybar/config.jsonc doesn't survive — HyDE's waybar.py regenerates it on --update, theme switches, and watcher restarts. Instead put a complete layout at ~/.config/waybar/layouts/12-custom.jsonc. waybar.py:46-51 searches ~/.config/waybar/layouts/ first, before HyDE's copy in ~/.local/share/. Files there are never overwritten.
Critical detail: define the workspace module inline at the top level of the layout, not via the include of HyDE's module file. Top-level keys beat included ones, so a HyDE refresh can't shadow it.
⚠️ Use ext/workspaces, not hyprland/workspaces
hyprland/workspaces switches workspaces by sending the legacy IPC string dispatch workspace N — the exact form the Lua migration turned into a syntax error. Every click silently did nothing. The string is hardcoded in the binary:
$ strings /usr/bin/waybar | grep 'dispatch workspace'
dispatch workspace
dispatch workspace name:
Waybar 0.15.0 is the newest available (pacman -Si waybar), so this cannot be fixed by config or by updating.
The fix is ext/workspaces, which drives the ext-workspace Wayland protocol instead of Hyprland IPC — no dispatch involved, so clicks work regardless. Both the compositor and waybar already support it (ext_workspace_manager_v1 in Hyprland, ext/workspaces compiled into waybar). It uses the same #workspaces button CSS, so theming carries over untouched.
"ext/workspaces": {
"all-outputs": false,
"active-only": false,
"ignore-hidden": false,
"sort-by-name": true,
"sort-by-id": false,
"format": "{name}",
"on-click": "activate",
"on-scroll-up": "hyprctl dispatch \"hl.dsp.focus({workspace = 'r-1'})\"",
"on-scroll-down": "hyprctl dispatch \"hl.dsp.focus({workspace = 'r+1'})\""
}
all-outputs: false makes each bar show only its own monitor's workspaces.
The one behavioural difference: ext/workspaces has no persistent-workspaces option. That job moved to the compositor — persistent = true on the workspace rules in hyprland.lua (see Workspaces above) is now what keeps empty workspaces visible as buttons. The two must stay in sync: a workspace missing a persistent rule will vanish from the bar when its last window closes.
Sort by name, not by id. sort-by-id takes precedence over every other sort option, and the "id" it sorts on is the ext-protocol handle — effectively creation order — which rendered DP-1 as 4 10 5 6. Names are "1"…"10", and waybar sorts all-numeric names numerically, giving the correct 4 5 6 10.
Note the scroll commands also had to be rewritten to the Lua dispatcher form, for the same reason the clicks broke.
Editing the layout is not enough. ~/.config/waybar/config.jsonc is generated from the layout, and restarting the waybar service does not regenerate it. After any layout edit:
hyde-shell waybar --set 12-custom
Verify the change actually landed by reading the generated file, not the layout:
python3 -c "import json,re;print(json.loads(re.sub(r'//.*','',open('$HOME/.config/waybar/config.jsonc').read()))['ext/workspaces'])"
Module placement in the same file:
group/pill#left.modules→[clock, wlr/taskbar, mpris]group/pill#center.modules→[hyprland/workspaces]group/pill#right2.modules→ original list +idle_inhibitor
hyde-shell waybar --set 12-custom
Verify: each monitor's bar shows only its own numbers; pgrep -c waybar → 1.
Font bump — ~/.config/waybar/user-style.css
style.css is HyDE-managed but imports user-style.css last.
* { font-size: 12px; } /* was 10px */
reload_style_on_change: true picks it up on save.
Caveat
hyde-shell waybar --next/--prev cycles through every discoverable layout including 12-custom. To stay pinned, don't invoke them.
Fastfetch — CachyOS logo
The system ships /usr/share/icons/cachyos.svg, but fastfetch's kitty graphics protocol needs raster:
mkdir -p ~/.config/fastfetch/logo
rsvg-convert -h 256 /usr/share/icons/cachyos.svg -o ~/.config/fastfetch/logo/cachyos.png
In ~/.config/fastfetch/config.jsonc:
"logo": {
"source": "~/.config/fastfetch/logo/cachyos.png",
"type": "kitty",
"height": 18
}
Renders only in a real kitty session — tmux/zellij strip the graphics protocol and it falls back to ASCII. The PNG lives under $HOME so package updates that rewrite the system SVG don't touch it.
Disk mounts — /etc/fstab
Procedure (reusable)
lsblk -f -o NAME,FSTYPE,LABEL,UUID,SIZE,MOUNTPOINT— use UUIDs, never/dev/sdX.sudo mkdir -p /mnt/<name>- Back up:
sudo cp /etc/fstab /etc/fstab.bak.$(date +%Y%m%d) - Append
UUID=… /mnt/<name> <fs> defaults,noatime,nofail 0 2nofailis critical — without it a missing drive drops boot into emergency mode.
sudo systemctl daemon-reload && sudo mount -a && mount | grep /mnt/- Optional:
sudo chown -R $USER:$USER /mnt/<name>
Occasionally-disconnected drives: x-systemd.automount,noauto. Encrypted drives need /etc/crypttab first.
Drives
| Device | UUID | FS | Mount |
|---|---|---|---|
/dev/sdb1 | 204b6728-1751-4fa3-b918-1349eb4e6172 | ext4 | /mnt/psi |
/dev/sda1 | 9aa68d02-8fba-435d-a5e7-34836a6c0473 | ext4 | /mnt/omega |
/dev/nvme0n1p1 | e34bdaae-8596-4e24-a643-35d2df1695f6 | ext4 | /mnt/alpha |
Status: ✅ Applied. All three are in /etc/fstab with nofail and mounted. Verify with findmnt /mnt/psi etc.
Verification after every HyDE update
The migration proved that config files lie. Check the running state:
hyprctl configerrors # MUST be empty — silent failures land here only
ls ~/.local/state/hyde/lua_state/ # want colors, hypr_theme, ui, shaders
hyde-shell validate # reports "no variables generated" breakage
hyprctl monitors | grep -E 'Monitor|scale' # 240Hz + 1.33 on DP-1?
hyprctl binds | grep -A2 'toggle monitor dim' # custom binds alive?
hyprctl workspaces -j | jq '[.[].id]|sort' # expect all 10 (persistent working)
hyprctl getoption decoration:inactive_opacity # expect 0.9, not 0.75
hyprctl getoption general:resize_on_border # expect true — theme actually loaded
hyprctl getoption decoration:rounding # expect 10; 0 means theme is dead
hyprctl animations | grep -c 'overridden: 1' # expect ~29; 5 means no preset loaded
pgrep -c hypridle; pgrep -c waybar # expect 1 and 1
Two independent silent-failure mechanisms bit here, and neither writes to the log:
- Hyprland rejecting a config field → shows only in
hyprctl configerrors. check_requirefinding no file → shows nowhere at all. This is what killed the theme and the animations. The only symptom is settings quietly sitting at Hyprland defaults, so check the values, not the files.
hyprctl configerrors is the single most important one. A rejected rule does not appear in hyprland.log — it shows up only there. Both opacity rules were silently discarded until that command was run.
Handy probes
Hyprland 0.56 exposes a Lua REPL — hyprctl repl <code> / hyprctl eval <code>:
# does a class regex actually match? tag windows and read the tags back
hyprctl repl 'hl.window_rule({name="__probe", match={class="^brave-browser$"}, tag="+__probe"}) return "ok"'
hyprctl clients -j | jq -r '.[] | "\(.class) \(.tags)"'
# clean up (reload alone will NOT clear tags already stamped on live windows)
hyprctl repl 'for _,w in ipairs(hl.get_windows()) do hl.dispatch(hl.dsp.window.clear_tags({window=w})) end return "cleared"'
Rule objects returned by hl.window_rule are not introspectable — their fields read back as nil. Tag-probing is the way to confirm a match. Windows also don't expose a resolved opacity property.
API reference: /usr/share/hypr/stubs/hl.meta.lua (type stubs) and ~/.local/share/hypr/lua/ (HyDE's own usage — key_binds.lua and window_rules.lua are the best examples).
Backups
Pre-migration copies of every file touched: ~/.config/hypr/backup-preLua-20260729/