Files
jaandJunie fe7f133136 feat(hyprland): migrate config from .conf to .lua format (Hyprland 0.55+)
- Add hyprland.lua.j2 Ansible template with full Lua DSL config
- Update tasks to deploy hyprland.lua instead of hyprland.conf
- Add docs/tech/hyprland-lua-migration.md with API reference,
  dispatcher mapping, pitfalls, and validation commands
- Update keybindings doc to reflect .lua config format

Co-authored-by: Junie <junie@jetbrains.com>
2026-06-15 06:43:23 +02:00

6.0 KiB

Hyprland Lua Config Migration (0.55+)

Summary

Hyprland 0.55 deprecates the legacy hyprlang (.conf) format in favor of a native Lua DSL (hyprland.lua). Both formats coexist during transition, but .lua is the future.

Configuration File Priority

File Format Priority
~/.config/hypr/hyprland.lua Lua DSL (hl.* API) Preferred (0.55+)
~/.config/hypr/hyprland.conf Legacy hyprlang Deprecated

When both exist, Hyprland loads .lua first. If .lua is present, .conf is ignored.

Lua API Quick Reference

-- Variables
local terminal = "kitty"
local mainMod = "SUPER"

-- Config blocks
hl.config({ general = { gaps_in = 5 } })
hl.env("XCURSOR_SIZE", "24")
hl.monitor({ output = "", mode = "preferred", position = "auto", scale = "auto" })

-- Keybindings (single combined key string)
hl.bind(mainMod .. " + Q", hl.dsp.window.close())
hl.bind(mainMod .. " + T", hl.dsp.exec_cmd(terminal))

-- Window/workspace rules
hl.window_rule({ name = "my-rule", match = { class = ".*" }, suppress_event = "maximize" })
hl.workspace_rule({ workspace = 1, monitor = "DP-1", default = true })

-- Autostart
hl.on("hyprland.start", function() hl.exec_cmd("waybar &") end)

-- Gestures, animations
hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace" })
hl.curve("easeOutQuint", { type = "bezier", points = { {0.23, 1}, {0.32, 1} } })
hl.animation({ leaf = "windows", enabled = true, speed = 4.79, spring = "easy" })

Dispatcher Functions (hl.dsp.*)

Lua API Legacy .conf equivalent
hl.dsp.exec_cmd("cmd") exec, cmd
hl.dsp.window.close() killactive
hl.dsp.window.float({ action = "toggle" }) togglefloating
hl.dsp.window.pseudo() pseudo
hl.dsp.window.fullscreen({ mode = 0 }) fullscreen, 0
hl.dsp.window.move({ direction = "left" }) movewindow, l
hl.dsp.window.move({ workspace = 2 }) movetoworkspace, 2
hl.dsp.window.drag() / .resize() mouse bind
hl.dsp.focus({ direction = "left" }) movefocus, l
hl.dsp.focus({ workspace = 2 }) workspace, 2
hl.dsp.focus({ workspace = "e+1" }) workspace, e+1
hl.dsp.workspace.toggle_special("magic") togglespecialworkspace
hl.dsp.layout("togglesplit") layoutmsg, togglesplit

Pitfalls Learned

Issue Fix
hl.dsp.focus({ direction = "next" }) Invalid. Only left/right/up/down accepted. Use hl.dsp.exec_cmd("hyprctl dispatch cyclenext") instead.
tap-to-click = true Invalid Lua. Hyphens not allowed in identifiers. Use tap_to_click (Hyprland accepts underscores).
hl.bind("", "Print", ...) Invalid. Lua API uses single key string: hl.bind("Print", ...).
hl.bind(mainMod, "Q", ...) Invalid. Combine into one string: hl.bind(mainMod .. " + Q", ...).
resizeactive not in hl.dsp.* Use hl.dsp.exec_cmd("hyprctl dispatch resizeactive -40 0") as wrapper.
luac -p false positives Standard luac can't validate Hyprland's custom hl.* API. Use hyprctl configerrors at runtime instead.

Validation

# Runtime validation (requires running Hyprland session)
hyprctl configerrors

# Over SSH (find instance signature first)
HYPRLAND_INSTANCE_SIGNATURE=$(ls /run/user/$(id -u)/hypr/) hyprctl configerrors

# Syntax-only check (limited - won't catch hl.* API issues)
luac -p ~/.config/hypr/hyprland.lua

conf-to-lua Migration Mapping

Legacy .conf Lua .lua
$mainMod = SUPER local mainMod = "SUPER"
monitor=DP-1,... hl.monitor({ output = "DP-1", ... })
env = KEY,VALUE hl.env("KEY", "VALUE")
input { ... } hl.config({ input = { ... } })
general { ... } hl.config({ general = { ... } })
bind = MOD, KEY, exec, cmd hl.bind(mod .. " + KEY", hl.dsp.exec_cmd("cmd"))
bind = MOD, KEY, killactive hl.bind(mod .. " + KEY", hl.dsp.window.close())
bind = MOD, KEY, togglefloating hl.bind(mod .. " + KEY", hl.dsp.window.float({ action = "toggle" }))
bind = MOD, KEY, fullscreen, 0 hl.bind(mod .. " + KEY", hl.dsp.window.fullscreen({ mode = 0 }))
bind = MOD, KEY, movefocus, l hl.bind(mod .. " + KEY", hl.dsp.focus({ direction = "left" }))
bind = MOD, KEY, movewindow, l hl.bind(mod .. " + KEY", hl.dsp.window.move({ direction = "left" }))
bind = MOD, KEY, workspace, N hl.bind(mod .. " + KEY", hl.dsp.focus({ workspace = N }))
bind = MOD, KEY, cyclenext hl.bind(mod .. " + KEY", hl.dsp.exec_cmd("hyprctl dispatch cyclenext"))
bind = MOD, KEY, pseudo hl.bind(mod .. " + KEY", hl.dsp.window.pseudo())
bind = MOD, KEY, layoutmsg, togglesplit hl.bind(mod .. " + KEY", hl.dsp.layout("togglesplit"))
bind = , Print, exec, cmd hl.bind("Print", hl.dsp.exec_cmd("cmd"))
bind = MOD, KEY, exit hl.bind(mod .. " + KEY", hl.dsp.exec_cmd("hyprctl dispatch exit"))
device { name = "..."; ... } hl.device({ name = "...", ... })
workspace = 1, monitor:DP-1, default:true hl.workspace_rule({ workspace = 1, monitor = "DP-1", default = true })
exec-once = cmd hl.on("hyprland.start", function() hl.exec_cmd("cmd") end)
col.active_border = rgba(...) 45deg col = { active_border = { colors = {"rgba(...)"}, angle = 45 } }

Multi-Machine Setup

This project manages configs for two machines with identical keybindings but different hardware:

Aspect Workstation (pfeddersheim) MacBook Pro 16,1
GPU NVIDIA (6 env vars) Intel UHD 630 + AMD RX 5500M (no env vars)
Monitors 3 fixed (DP-1, DVI-D-1, HDMI-A-1) Built-in + HDMI dock (auto-detect)
Input de layout, left-handed mouse de + mac_nodeadkeys + caps:swapescape, touchpad
Touchpad N/A natural_scroll, gestures, disable_while_typing
Media keys N/A Volume, brightness, playerctl
Window rules None Suppress maximize, XWayland fix
Config format .lua (Ansible-managed) .lua (manual)