From fe7f133136ee063720fe1850781a178175687ad3 Mon Sep 17 00:00:00 2001 From: Jane Alesi Date: Mon, 15 Jun 2026 06:43:23 +0200 Subject: [PATCH] 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 --- ansible/roles/hyprland/tasks/main.yml | 2 +- .../roles/hyprland/templates/hyprland.lua.j2 | 190 ++++++++++++++++++ docs/hyprland-keybindings.md | 12 +- docs/tech/hyprland-lua-migration.md | 126 ++++++++++++ 4 files changed, 324 insertions(+), 6 deletions(-) create mode 100644 ansible/roles/hyprland/templates/hyprland.lua.j2 create mode 100644 docs/tech/hyprland-lua-migration.md diff --git a/ansible/roles/hyprland/tasks/main.yml b/ansible/roles/hyprland/tasks/main.yml index 5825585..b197dc2 100644 --- a/ansible/roles/hyprland/tasks/main.yml +++ b/ansible/roles/hyprland/tasks/main.yml @@ -88,7 +88,7 @@ group: "{{ system_user }}" mode: '0644' loop: - - { src: 'hyprland.conf.j2', dest: 'hypr/hyprland.conf' } + - { src: 'hyprland.lua.j2', dest: 'hypr/hyprland.lua' } - { src: 'hyprlock.conf.j2', dest: 'hypr/hyprlock.conf' } - { src: 'hypridle.conf.j2', dest: 'hypr/hypridle.conf' } - { src: 'hyprpaper.conf.j2', dest: 'hypr/hyprpaper.conf' } diff --git a/ansible/roles/hyprland/templates/hyprland.lua.j2 b/ansible/roles/hyprland/templates/hyprland.lua.j2 new file mode 100644 index 0000000..93292f8 --- /dev/null +++ b/ansible/roles/hyprland/templates/hyprland.lua.j2 @@ -0,0 +1,190 @@ +-- ========================================================================= +-- hyprland.lua - managed by Ansible +-- Hyprland 0.55+ Lua config for mw-pfeddersheim-workstation +-- SUPER = Windows key +-- ========================================================================= + +local terminal = "kitty" +local fileManager = "kitty -e yazi" +local menu = "hyprlauncher" +local browser = "google-chrome-stable" +local mainMod = "SUPER" + +----------------- +---- MONITORS ---- +----------------- + +-- 1. DP-1 TERRA 3280W 2560x1440@144 (0x0) +-- 2. DVI-D-1 BenQ BL2405 1920x1080@60 (rotated 90 CW) +-- 3. HDMI-A-1 TERRA 3280W 2560x1440@144 (3640x0) +hl.monitor({ output = "DP-1", mode = "2560x1440@60", position = "0x0", scale = "1" }) +hl.monitor({ output = "DVI-D-1", mode = "1920x1080@60", position = "2560x0", scale = "1", transform = "1" }) +hl.monitor({ output = "HDMI-A-1", mode = "2560x1440@60", position = "3640x0", scale = "1" }) + +hl.workspace_rule({ workspace = 1, monitor = "DP-1", default = true }) +hl.workspace_rule({ workspace = 2, monitor = "DVI-D-1", default = true }) +hl.workspace_rule({ workspace = 3, monitor = "HDMI-A-1", default = true }) + +------------------------------- +---- ENVIRONMENT VARIABLES ---- +------------------------------- + +hl.env("LIBVA_DRIVER_NAME", "nvidia") +hl.env("XDG_SESSION_TYPE", "wayland") +hl.env("GBM_BACKEND", "nvidia-drm") +hl.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia") +hl.env("WLR_NO_HARDWARE_CURSORS", "1") +hl.env("NVD_BACKEND", "direct") + +--------------- +---- INPUT ---- +--------------- + +hl.config({ + input = { + kb_layout = "de", + follow_mouse = 1, + sensitivity = 0, + numlock_by_default = true, + }, +}) + +hl.device({ name = "pixart-usb-optical-mouse", left_handed = 1 }) + +----------------------- +---- LOOK AND FEEL ---- +----------------------- + +hl.config({ + general = { + gaps_in = 5, gaps_out = 10, border_size = 2, + col = { + active_border = { colors = {"rgba(33ccffee)", "rgba(00ff99ee)"}, angle = 45 }, + inactive_border = "rgba(595959aa)", + }, + resize_on_border = false, allow_tearing = false, layout = "dwindle", + }, + decoration = { + rounding = 10, rounding_power = 2, + active_opacity = 1.0, inactive_opacity = 1.0, + shadow = { enabled = true, range = 4, render_power = 3, color = 0xee1a1a1a }, + blur = { enabled = true, size = 3, passes = 1, vibrancy = 0.1696 }, + }, + animations = { enabled = true }, +}) + +hl.curve("easeOutQuint", { type = "bezier", points = { {0.23,1},{0.32,1} } }) +hl.curve("easeInOutCubic", { type = "bezier", points = { {0.65,0.05},{0.36,1} } }) +hl.curve("linear", { type = "bezier", points = { {0,0},{1,1} } }) +hl.curve("almostLinear", { type = "bezier", points = { {0.5,0.5},{0.75,1} } }) +hl.curve("quick", { type = "bezier", points = { {0.15,0},{0.1,1} } }) +hl.curve("easy", { type = "spring", mass = 1, stiffness = 71.2633, dampening = 15.8273644 }) + +hl.animation({ leaf = "global", enabled = true, speed = 10, bezier = "default" }) +hl.animation({ leaf = "border", enabled = true, speed = 5.39, bezier = "easeOutQuint" }) +hl.animation({ leaf = "windows", enabled = true, speed = 4.79, spring = "easy" }) +hl.animation({ leaf = "windowsIn", enabled = true, speed = 4.1, spring = "easy", style = "popin 87%" }) +hl.animation({ leaf = "windowsOut", enabled = true, speed = 1.49, bezier = "linear", style = "popin 87%" }) +hl.animation({ leaf = "fadeIn", enabled = true, speed = 1.73, bezier = "almostLinear" }) +hl.animation({ leaf = "fadeOut", enabled = true, speed = 1.46, bezier = "almostLinear" }) +hl.animation({ leaf = "fade", enabled = true, speed = 3.03, bezier = "quick" }) +hl.animation({ leaf = "layers", enabled = true, speed = 3.81, bezier = "easeOutQuint" }) +hl.animation({ leaf = "layersIn", enabled = true, speed = 4, bezier = "easeOutQuint", style = "fade" }) +hl.animation({ leaf = "layersOut", enabled = true, speed = 1.5, bezier = "linear", style = "fade" }) +hl.animation({ leaf = "fadeLayersIn", enabled = true, speed = 1.79, bezier = "almostLinear" }) +hl.animation({ leaf = "fadeLayersOut", enabled = true, speed = 1.39, bezier = "almostLinear" }) +hl.animation({ leaf = "workspaces", enabled = true, speed = 1.94, bezier = "almostLinear", style = "fade" }) +hl.animation({ leaf = "workspacesIn", enabled = true, speed = 1.21, bezier = "almostLinear", style = "fade" }) +hl.animation({ leaf = "workspacesOut", enabled = true, speed = 1.94, bezier = "almostLinear", style = "fade" }) +hl.animation({ leaf = "zoomFactor", enabled = true, speed = 7, bezier = "quick" }) + +hl.config({ dwindle = { preserve_split = true } }) +hl.config({ master = { new_status = "master" } }) +hl.config({ misc = { force_default_wallpaper = 0, disable_hyprland_logo = true } }) + +--------------------- +---- KEYBINDINGS ---- +--------------------- + +-- Core binds +hl.bind(mainMod .. " + Q", hl.dsp.window.close()) +hl.bind(mainMod .. " + T", hl.dsp.exec_cmd(terminal)) +hl.bind(mainMod .. " + RETURN", hl.dsp.exec_cmd(terminal)) +hl.bind(mainMod .. " + M", hl.dsp.exec_cmd("command -v hyprshutdown >/dev/null 2>&1 && hyprshutdown || hyprctl dispatch 'hl.dsp.exit()'")) +hl.bind(mainMod .. " + E", hl.dsp.exec_cmd(fileManager)) +hl.bind(mainMod .. " + V", hl.dsp.window.float({ action = "toggle" })) +hl.bind(mainMod .. " + D", hl.dsp.exec_cmd(menu)) +hl.bind(mainMod .. " + P", hl.dsp.window.pseudo()) +hl.bind(mainMod .. " + J", hl.dsp.layout("togglesplit")) + +-- Worktime safers +hl.bind(mainMod .. " + F", hl.dsp.window.fullscreen({ mode = 0 })) +hl.bind(mainMod .. " + R", hl.dsp.exec_cmd("hyprctl reload")) +hl.bind(mainMod .. " + SPACE", hl.dsp.window.float({ action = "toggle" })) +hl.bind(mainMod .. " + TAB", hl.dsp.exec_cmd("hyprctl dispatch cyclenext")) +hl.bind(mainMod .. " + SHIFT + TAB", hl.dsp.exec_cmd("hyprctl dispatch cyclenext prev")) + +-- Scratchpad +hl.bind(mainMod .. " + GRAVE", hl.dsp.workspace.toggle_special("magic")) +hl.bind(mainMod .. " + SHIFT + GRAVE", hl.dsp.window.move({ workspace = "special:magic" })) + +-- Home row focus +hl.bind(mainMod .. " + H", hl.dsp.focus({ direction = "left" })) +hl.bind(mainMod .. " + L", hl.dsp.focus({ direction = "right" })) + +-- Home row move window +hl.bind(mainMod .. " + SHIFT + H", hl.dsp.window.move({ direction = "left" })) +hl.bind(mainMod .. " + SHIFT + L", hl.dsp.window.move({ direction = "right" })) +hl.bind(mainMod .. " + SHIFT + K", hl.dsp.window.move({ direction = "up" })) +hl.bind(mainMod .. " + SHIFT + J", hl.dsp.window.move({ direction = "down" })) + +-- Home row resize +hl.bind(mainMod .. " + ALT + H", hl.dsp.exec_cmd("hyprctl dispatch resizeactive -40 0")) +hl.bind(mainMod .. " + ALT + L", hl.dsp.exec_cmd("hyprctl dispatch resizeactive 40 0")) +hl.bind(mainMod .. " + ALT + K", hl.dsp.exec_cmd("hyprctl dispatch resizeactive 0 -40")) +hl.bind(mainMod .. " + ALT + J", hl.dsp.exec_cmd("hyprctl dispatch resizeactive 0 40")) + +-- Arrow focus +hl.bind(mainMod .. " + left", hl.dsp.focus({ direction = "left" })) +hl.bind(mainMod .. " + right", hl.dsp.focus({ direction = "right" })) +hl.bind(mainMod .. " + up", hl.dsp.focus({ direction = "up" })) +hl.bind(mainMod .. " + down", hl.dsp.focus({ direction = "down" })) + +-- Arrow move window +hl.bind(mainMod .. " + SHIFT + left", hl.dsp.window.move({ direction = "left" })) +hl.bind(mainMod .. " + SHIFT + right", hl.dsp.window.move({ direction = "right" })) +hl.bind(mainMod .. " + SHIFT + up", hl.dsp.window.move({ direction = "up" })) +hl.bind(mainMod .. " + SHIFT + down", hl.dsp.window.move({ direction = "down" })) + +-- Lock, browser +hl.bind(mainMod .. " + CTRL + L", hl.dsp.exec_cmd("hyprlock")) +hl.bind(mainMod .. " + B", hl.dsp.exec_cmd(browser)) + +-- Workspaces 1-9 +for i = 1, 9 do + hl.bind(mainMod .. " + " .. i, hl.dsp.focus({ workspace = i })) + hl.bind(mainMod .. " + SHIFT + " .. i, hl.dsp.window.move({ workspace = i })) +end + +-- Screenshots +hl.bind("Print", hl.dsp.exec_cmd("grimblast copysave area")) +hl.bind(mainMod .. " + Print", hl.dsp.exec_cmd("grimblast copysave active")) +hl.bind(mainMod .. " + SHIFT + Print", hl.dsp.exec_cmd("grimblast copysave output")) +hl.bind(mainMod .. " + ALT + Print", hl.dsp.exec_cmd('grim -g "$(slurp)" - | satty -f -')) + +-------------------------------- +---- WINDOW RULES ---- +-------------------------------- + +hl.window_rule({ + name = "suppress-maximize-events", + match = { class = ".*" }, + suppress_event = "maximize", +}) + +hl.window_rule({ + name = "fix-xwayland-drags", + match = { class = "^$", title = "^$", xwayland = true, float = true, fullscreen = false, pin = false }, + no_focus = true, +}) + diff --git a/docs/hyprland-keybindings.md b/docs/hyprland-keybindings.md index deadcb7..cafce53 100644 --- a/docs/hyprland-keybindings.md +++ b/docs/hyprland-keybindings.md @@ -1,6 +1,6 @@ -# Hyprland Keybindings — Full Cheatsheet +# Hyprland Keybindings - Full Cheatsheet -**Layout:** German (de) | **Mod key:** SUPER (Windows key) | **Source:** `hyprctl binds` live +**Layout:** German (de) | **Mod key:** SUPER (Windows key) | **Config:** `hyprland.lua` (Hyprland 0.55+) --- @@ -24,7 +24,7 @@ | `SUPER+SHIFT+TAB` | Cycle previous window | cyclenext prev | | `SUPER+` `` ` `` (Grave) | Toggle scratchpad | togglespecialworkspace | | `SUPER+SHIFT+` `` ` `` | Send window to scratchpad | movetoworkspace special | -| `SUPER+CTRL+K` | Keybind cheat menu | *(broken — pipelines to grep+fuzzel)* | +| `SUPER+CTRL+K` | Keybind cheat menu | *(broken - pipelines to grep+fuzzel)* | | `SUPER+CTRL+L` | Lock screen | hyprlock | | `SUPER+B` | Web browser | google-chrome-stable | @@ -103,8 +103,10 @@ Every kitty terminal tile shows a title bar with: ## Notes -- `SUPER+CTRL+K` (keybind menu) does **not work** — the `sed` pipeline breaks. Needs fix. -- The `sed` pipeline `sed 's/^bind\s*=\s*\$mainMod/SUPER/;s/\s*#/#/'` fails silently because the unescaped `$` in the `sed` command gets interpreted as a variable. +- `SUPER+CTRL+K` (keybind menu) does **not work** - the `sed` pipeline breaks. + The Lua config format makes this harder to grep; consider a different approach. +- Config migrated from `.conf` to `.lua` format (Hyprland 0.55+). + See `docs/tech/hyprland-lua-migration.md` for the migration mapping. ## Troubleshooting diff --git a/docs/tech/hyprland-lua-migration.md b/docs/tech/hyprland-lua-migration.md new file mode 100644 index 0000000..b01169d --- /dev/null +++ b/docs/tech/hyprland-lua-migration.md @@ -0,0 +1,126 @@ +# 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 + +```lua +-- 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 + +```bash +# 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) | \ No newline at end of file