Files
hyprtron/HYPRLAND_LUA_MIGRATION.md
T

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

  1. Modularity: Lua uses require("module_name") for imports instead of source = .... A file bindings.lua is imported via require("bindings"). Subdirectories work via dot-notation: require("bindings.media") maps to bindings/media.lua.
  2. Extensions: Create new .lua files mirroring the names of the old .conf files.
  3. Scope: Only Hyprland config files are migrated to Lua (e.g., hyprland.conf, monitors.conf, bindings.conf). Do not migrate configurations for external tools like hypridle.conf, hyprlock.conf, waybar, or hyprpaper.

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:

  1. 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
    
  2. Restart Notification: Inform the user that because the configuration language is shifting from .conf to .lua, Hyprland will only detect this on a fresh startup. The user MUST log out and log back in to activate the new configuration.

  3. 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
      
  4. Cleanup Phase: Once the user confirms the Lua configuration is active and working correctly, instruct the user to remove the old .conf files (or do it for them if requested). Leave hypridle.conf, hyprlock.conf, and hyprpaper.conf intact!