9.4 KiB
Hyprland Lua Migration Guide for AI Agents
Objective
Migrate an existing legacy Hyprland configuration (using .conf files based on the old hyprlang syntax) to the new Lua-based configuration system introduced in Hyprland v0.55+.
CRITICAL INSTRUCTION FOR THE AGENT: The target machine has a slightly different configuration (e.g., different monitors, workspaces, applications, keybindings) than the original machine this guide was generated on. Do NOT blindly copy-paste the examples below. Use them only as a translation reference to convert the exact values found on the target machine into the new Lua syntax.
Migration Principles
- Modularity: Lua uses
require("module_name")for imports instead ofsource = .... A filebindings.luais imported viarequire("bindings"). Subdirectories work via dot-notation:require("bindings.media")maps tobindings/media.lua. - Extensions: Create new
.luafiles mirroring the names of the old.conffiles. - Scope: Only Hyprland config files are migrated to Lua (e.g.,
hyprland.conf,monitors.conf,bindings.conf). Do not migrate configurations for external tools likehypridle.conf,hyprlock.conf,waybar, orhyprpaper.
Syntax Translation Reference
1. Variables and Environment Variables
Legacy (.conf):
$terminal = kitty
env = GDK_SCALE,1
Lua (.lua):
local terminal = "kitty"
hl.env("GDK_SCALE", "1")
2. Autostart (exec-once)
Legacy:
exec-once = uwsm app -- waybar
exec-once = uwsm app -- hyprpaper
Lua:
hl.on("hyprland.start", function ()
hl.exec_cmd("uwsm app -- waybar")
hl.exec_cmd("uwsm app -- hyprpaper")
end)
3. Monitors
Legacy:
monitor=eDP-1,1920x1080,auto,1
monitor=desc:Xiaomi Corporation Mi Monitor,3440x1440@60,auto,1
Lua:
hl.monitor({ output = "eDP-1", mode = "1920x1080", position = "auto", scale = 1 })
hl.monitor({ output = "desc:Xiaomi Corporation Mi Monitor", mode = "3440x1440@60", position = "auto", scale = 1 })
4. Configuration Blocks (general, decoration, etc.)
Legacy:
general {
gaps_in = 4
col.active_border = rgba(33ccffee)
layout = dwindle
}
decoration {
rounding = 4
blur {
enabled = true
size = 8
}
}
Lua:
hl.config({
general = {
gaps_in = 4,
col = {
active_border = "rgba(33ccffee)",
},
layout = "dwindle",
},
decoration = {
rounding = 4,
blur = {
enabled = true,
size = 8,
},
},
})
5. Keybindings (bind, bindd, bindm, bindl, binde)
Legacy:
bind = SUPER, Q, killactive,
bind = SUPER SHIFT ALT, Q, exit,
bindd = SUPER, B, Launch Browser, exec, firefox
bind = SUPER, left, movefocus, l
bind = SUPER, 1, workspace, 1
bind = SUPER SHIFT, 1, movetoworkspace, 1
bind = SUPER SHIFT, left, swapwindow, l
bind = SUPER, mouse_down, workspace, e+1
bindm = SUPER, mouse:272, movewindow
bindm = SUPER, mouse:273, resizewindow
bindel = ,XF86AudioRaiseVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+
bind = SUPER, code:10, workspace, 1
bind = SUPER, code:20, resizeactive, -100 0
bind = SUPER, BACKSPACE, setprop, opaque toggle
Lua:
Flags from the legacy format (e for repeat, l for locked, m for mouse) become booleans in the options table. Dispatchers must use the native hl.dsp.* API functions.
-- Session & Window Lifecycle
hl.bind("SUPER + Q", hl.dsp.window.close(), { description = "Close active window" })
hl.bind("SUPER + SHIFT + ALT + Q", hl.dsp.exit(), { description = "Exit Hyprland" })
-- Applications / Commands
hl.bind("SUPER + B", hl.dsp.exec_cmd("firefox"), { description = "Launch Browser" })
-- Focus & Workspaces
hl.bind("SUPER + left", hl.dsp.focus({ direction = "left" }))
hl.bind("SUPER + 1", hl.dsp.focus({ workspace = 1 }))
hl.bind("SUPER + TAB", hl.dsp.focus({ workspace = "e+1" }))
hl.bind("SUPER + mouse_down", hl.dsp.focus({ workspace = "e+1" }))
-- Moving Windows
hl.bind("SUPER + SHIFT + 1", hl.dsp.window.move({ workspace = 1 }))
hl.bind("SUPER + SHIFT + left", hl.dsp.window.swap({ direction = "left" }))
-- Window States
hl.bind("SUPER + V", hl.dsp.window.float({ action = "toggle" }))
hl.bind("SUPER + P", hl.dsp.window.pseudo())
hl.bind("SHIFT + F11", hl.dsp.window.fullscreen({ mode = "fullscreen" }))
hl.bind("ALT + F11", hl.dsp.window.fullscreen({ mode = "maximized" }))
hl.bind("SUPER + BACKSPACE", hl.dsp.window.set_prop({ prop = "opaque", value = "toggle" }))
-- Window Cycling & Stacking
hl.bind("ALT + TAB", hl.dsp.window.cycle_next())
hl.bind("ALT + SHIFT + TAB", hl.dsp.window.cycle_next({ next = false }))
hl.bind("ALT + TAB", hl.dsp.window.bring_to_top())
-- Resizing & Dragging
hl.bind("SUPER + code:20", hl.dsp.window.resize({ x = -100, y = 0, relative = true }))
hl.bind("SUPER + mouse:272", hl.dsp.window.drag(), { mouse = true })
hl.bind("SUPER + mouse:273", hl.dsp.window.resize(), { mouse = true })
-- Repeating & Locked (e.g. Media Keys)
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+"), { repeating = true, locked = true })
Note: Key combinations are joined by +.
Complete Dispatcher Reference:
exit->hl.dsp.exit()killactive->hl.dsp.window.close()togglefloating->hl.dsp.window.float({ action = "toggle" })pseudo->hl.dsp.window.pseudo()fullscreen, 0->hl.dsp.window.fullscreen({ mode = "fullscreen" })fullscreen, 1->hl.dsp.window.fullscreen({ mode = "maximized" })togglesplit/layoutmsg, <cmd>->hl.dsp.layout("<cmd>")movefocus, <l|r|u|d>->hl.dsp.focus({ direction = "left" | "right" | "up" | "down" })workspace, <ws>->hl.dsp.focus({ workspace = <number or string like "e+1"> })movetoworkspace, <ws>->hl.dsp.window.move({ workspace = <number or string> })swapwindow, <l|r|u|d>->hl.dsp.window.swap({ direction = "left" | "right" | "up" | "down" })cyclenext->hl.dsp.window.cycle_next()cyclenext, prev->hl.dsp.window.cycle_next({ next = false })bringactivetotop->hl.dsp.window.bring_to_top()resizeactive, <x> <y>->hl.dsp.window.resize({ x = <x>, y = <y>, relative = true })setprop, <prop> <val>->hl.dsp.window.set_prop({ prop = "<prop>", value = "<val>" })togglespecialworkspace, <name>->hl.dsp.workspace.toggle_special("<name>")exec, <command>->hl.dsp.exec_cmd("<command>")
6. Window and Layer Rules
Legacy:
layerrule = blur true, match:namespace waybar
windowrule = opacity 0.85 0.8,match:class ^chrome-gemini\.google\.com.*
windowrule = float true,match:class ^org\.gnome\.Calculator$
windowrule = no_focus true,match:class ^$,match:title ^$,match:xwayland true
Lua:
Watch out for string escaping in regex! Single backslashes in regex must become double backslashes in Lua strings. Multi-matches are grouped into the match table.
hl.layer_rule({ match = { namespace = "waybar" }, blur = true })
hl.window_rule({ match = { class = "^chrome-gemini\\.google\\.com.*" }, opacity = "0.85 0.8" })
hl.window_rule({ match = { class = "^org\\.gnome\\.Calculator$" }, float = true })
hl.window_rule({
match = { class = "^$", title = "^$", xwayland = true },
no_focus = true
})
7. Advanced Features (Animations, Devices, Gestures)
Legacy Animations:
bezier = easeOutQuint, 0.23, 1, 0.32, 1
animation = windows, 1, 4.79, easeOutQuint
animation = windowsIn, 1, 4.1, easeOutQuint, popin 87%
Lua Animations: Note: Speed 1 in legacy = 100ms in lua. 4.79 -> 4.79.
hl.curve("easeOutQuint", { type = "bezier", points = {{0.23, 1}, {0.32, 1}} })
hl.animation({ leaf = "windows", enabled = true, speed = 4.79, bezier = "easeOutQuint" })
hl.animation({ leaf = "windowsIn", enabled = true, speed = 4.1, bezier = "easeOutQuint", style = "popin 87%" })
Legacy Devices & Gestures:
device {
name = epic-mouse-v1
sensitivity = -0.5
}
gesture = 3, horizontal, workspace
Lua Devices & Gestures:
hl.device({
name = "epic-mouse-v1",
sensitivity = -0.5
})
hl.gesture({
fingers = 3,
direction = "horizontal",
action = "workspace"
})
Validation Steps (Post-Migration)
Once the agent has translated the .conf files into .lua files, it MUST perform the following validations before declaring the task complete:
-
Syntax Check: Use the built-in Lua compiler to ensure there are no missing brackets or syntax errors.
luac -p ~/.config/hypr/hyprland.lua # Run this for every .lua file generated -
Restart Notification: Inform the user that because the configuration language is shifting from
.confto.lua, Hyprland will only detect this on a fresh startup. The user MUST log out and log back in to activate the new configuration. -
Runtime Validation Commands: Provide the user with these commands to verify the migration worked once they log back in:
- Check if Lua is the active config provider:
hyprctl systeminfo | grep -i config # Should output: configProvider: lua - Check if Lua keybindings are registered:
hyprctl binds | grep -i "__lua" | head -n 5
- Check if Lua is the active config provider:
-
Cleanup Phase: Once the user confirms the Lua configuration is active and working correctly, instruct the user to remove the old
.conffiles (or do it for them if requested). Leavehypridle.conf,hyprlock.conf, andhyprpaper.confintact!