hyprland: migrate settings to native lua option API

This commit is contained in:
Austin Horstman 2026-05-12 12:45:58 -05:00
parent 7654d90b94
commit acd9d8af2f
9 changed files with 576 additions and 78 deletions

View file

@ -64,3 +64,9 @@ changes are only active if the `home.stateVersion` option is set to
- On Linux, `programs.firefox.configPath` now defaults to
`"${config.xdg.configHome}/mozilla/firefox"` for `home.stateVersion = "26.05"` and later.
Older state versions keep the legacy default of `".mozilla/firefox"`.
- The [](#opt-wayland.windowManager.hyprland.configType) option now
defaults to `"lua"` for `home.stateVersion = "26.05"` and later.
Older state versions keep the legacy `"hyprlang"` default. Set
`wayland.windowManager.hyprland.configType = "hyprlang"` to keep
generating {file}`$XDG_CONFIG_HOME/hypr/hyprland.conf`.

View file

@ -5,13 +5,26 @@
...
}:
let
cfg = config.wayland.windowManager.hyprland;
toLua = lib.generators.toLua { };
variables = builtins.concatStringsSep " " cfg.systemd.variables;
extraCommands = builtins.concatStringsSep " " (map (f: "&& ${f}") cfg.systemd.extraCommands);
systemdActivation = ''
exec-once = ${pkgs.dbus}/bin/dbus-update-activation-environment --systemd ${variables} ${extraCommands}
systemdActivationCommand = "${pkgs.dbus}/bin/dbus-update-activation-environment --systemd ${variables} ${extraCommands}";
pluginPath =
entry: if lib.types.package.check entry then "${entry}/lib/lib${entry.pname}.so" else entry;
reloadConfig = ''
(
XDG_RUNTIME_DIR=''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}
if [[ -d "/tmp/hypr" || -d "$XDG_RUNTIME_DIR/hypr" ]]; then
for i in $(${cfg.finalPackage}/bin/hyprctl instances -j | jq ".[].instance" -r); do
${cfg.finalPackage}/bin/hyprctl -i "$i" reload config-only
done
fi
)
'';
in
{
@ -118,6 +131,35 @@ in
'';
};
configType = lib.mkOption {
type = lib.types.enum [
"hyprlang"
"lua"
];
inherit
(lib.hm.deprecations.mkStateVersionOptionDefault {
inherit (config.home) stateVersion;
since = "26.05";
optionPath = [
"wayland"
"windowManager"
"hyprland"
"configType"
];
legacy.value = "hyprlang";
current.value = "lua";
})
default
defaultText
;
description = ''
The type of Hyprland configuration to generate.
`hyprlang` writes {file}`$XDG_CONFIG_HOME/hypr/hyprland.conf`.
`lua` writes {file}`$XDG_CONFIG_HOME/hypr/hyprland.lua`.
'';
};
systemd = {
enable = lib.mkEnableOption null // {
default = true;
@ -206,22 +248,69 @@ in
declare plugins.
:::
When {option}`wayland.windowManager.hyprland.configType` is `"lua"`,
each attribute maps to an `hl.<name>(...)` call. List values generate
one call per element.
Attribute values with an `_args` list generate multi-argument calls.
Values created with `lib.generators.mkLuaInline` are rendered as raw
Lua expressions.
'';
example = lib.literalExpression ''
{
decoration = {
shadow_offset = "0 5";
"col.shadow" = "rgba(00000099)";
config = {
general = {
gaps_in = 5;
gaps_out = 20;
border_size = 2;
};
decoration = {
rounding = 10;
};
};
"$mod" = "SUPER";
bindm = [
# mouse movements
"$mod, mouse:272, movewindow"
"$mod, mouse:273, resizewindow"
"$mod ALT, mouse:272, resizewindow"
bind = [
{
_args = [
"SUPER + Q"
(lib.generators.mkLuaInline "hl.dsp.window.close()")
{ locked = true; }
];
}
{
_args = [
"SUPER + RETURN"
(lib.generators.mkLuaInline "hl.dsp.exec_cmd(\"kitty\")")
];
}
{
_args = [
"ALT + R"
(lib.generators.mkLuaInline "hl.dsp.submap(\"resize\")")
];
}
];
define_submap = {
_args = [
"resize"
(lib.generators.mkLuaInline "function()\n hl.bind(\"right\", hl.dsp.window.resize({ x = 10, y = 0, relative = true }), { repeating = true })\n hl.bind(\"left\", hl.dsp.window.resize({ x = -10, y = 0, relative = true }), { repeating = true })\n hl.bind(\"escape\", hl.dsp.submap(\"reset\"))\nend")
];
};
window_rule = {
match.class = "kitty";
border_size = 2;
};
on = {
_args = [
"hyprland.start"
(lib.generators.mkLuaInline "function()\n hl.exec_cmd(\"waybar\")\nend")
];
};
}
'';
};
@ -230,7 +319,7 @@ in
description = ''
Attribute set of Hyprland submaps.
See <https://wiki.hypr.land/Configuring/Basics/Binds/#submaps> to learn about submaps
See <https://wiki.hypr.land/Configuring/Basics/Binds/#submaps> to learn about submaps.
'';
default = { };
type = lib.types.attrsOf (
@ -250,7 +339,7 @@ in
};
default = { };
description = ''
Hyprland binds to be put in the submap
Hyprland binds to be put in the submap.
'';
example = lib.literalExpression ''
{
@ -299,19 +388,17 @@ in
type = lib.types.lines;
default = "";
example = ''
# window resize
bind = $mod, S, submap, resize
submap = resize
binde = , right, resizeactive, 10 0
binde = , left, resizeactive, -10 0
binde = , up, resizeactive, 0 -10
binde = , down, resizeactive, 0 10
bind = , escape, submap, reset
submap = reset
hl.on("window.open", function()
hl.timer(function()
hl.dispatch(hl.dsp.exec_cmd("notify-send 'Window opened'"))
end, {
timeout = 100,
type = "oneshot",
})
end)
'';
description = ''
Extra configuration lines to add to `~/.config/hypr/hyprland.conf`.
Extra configuration content appended to the generated Hyprland file.
'';
};
@ -328,6 +415,7 @@ in
default = [
"$"
"bezier"
"curve"
"name"
"output"
];
@ -377,63 +465,111 @@ in
[ cfg.finalPackage ] ++ lib.optional cfg.xwayland.enable pkgs.xwayland
);
xdg.configFile."hypr/hyprland.conf" =
let
shouldGenerate =
cfg.systemd.enable || cfg.extraConfig != "" || cfg.settings != { } || cfg.plugins != [ ];
xdg.configFile = lib.mkMerge [
{
"hypr/hyprland.conf" = lib.mkIf (cfg.configType == "hyprlang") (
let
shouldGenerate =
cfg.systemd.enable || cfg.extraConfig != "" || cfg.settings != { } || cfg.plugins != [ ];
importantPrefixes = cfg.importantPrefixes ++ lib.optional cfg.sourceFirst "source";
importantPrefixes = cfg.importantPrefixes ++ lib.optional cfg.sourceFirst "source";
pluginsToHyprconf =
plugins:
lib.hm.generators.toHyprconf {
attrs = {
"exec-once" =
let
mkEntry =
entry: if lib.types.package.check entry then "${entry}/lib/lib${entry.pname}.so" else entry;
in
map (p: "hyprctl plugin load ${mkEntry p}") plugins;
};
inherit importantPrefixes;
};
pluginsToHyprconf =
plugins:
lib.hm.generators.toHyprconf {
attrs = {
"exec-once" = map (entry: "hyprctl plugin load ${pluginPath entry}") plugins;
};
inherit importantPrefixes;
};
mkSubMap = name: attrs: ''
submap = ${name}${lib.optionalString (attrs.onDispatch != "") ", ${attrs.onDispatch}"}
${
lib.hm.generators.toHyprconf {
attrs = attrs.settings;
indentLevel = 0;
}
}submap = reset
'';
mkSubMap = name: attrs: ''
submap = ${name}${lib.optionalString (attrs.onDispatch != "") ", ${attrs.onDispatch}"}
${
lib.hm.generators.toHyprconf {
attrs = attrs.settings;
indentLevel = 0;
}
}submap = reset
'';
submapsToHyprConf = lib.concatMapAttrsStringSep "\n" mkSubMap;
in
lib.mkIf shouldGenerate {
text =
lib.optionalString cfg.systemd.enable systemdActivation
+ lib.optionalString (cfg.plugins != [ ]) (pluginsToHyprconf cfg.plugins)
+ lib.optionalString (cfg.settings != { }) (
lib.hm.generators.toHyprconf {
attrs = cfg.settings;
inherit importantPrefixes;
}
)
+ lib.optionalString (cfg.submaps != { }) (submapsToHyprConf cfg.submaps)
+ lib.optionalString (cfg.extraConfig != "") cfg.extraConfig;
submapsToHyprConf = lib.concatMapAttrsStringSep "\n" mkSubMap;
in
lib.mkIf shouldGenerate {
text =
lib.optionalString cfg.systemd.enable ''
exec-once = ${systemdActivationCommand}
''
+ lib.optionalString (cfg.plugins != [ ]) (pluginsToHyprconf cfg.plugins)
+ lib.optionalString (cfg.settings != { }) (
lib.hm.generators.toHyprconf {
attrs = cfg.settings;
inherit importantPrefixes;
}
)
+ lib.optionalString (cfg.submaps != { }) (submapsToHyprConf cfg.submaps)
+ lib.optionalString (cfg.extraConfig != "") cfg.extraConfig;
onChange = lib.mkIf (cfg.package != null) ''
(
XDG_RUNTIME_DIR=''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}
if [[ -d "/tmp/hypr" || -d "$XDG_RUNTIME_DIR/hypr" ]]; then
for i in $(${cfg.finalPackage}/bin/hyprctl instances -j | jq ".[].instance" -r); do
${cfg.finalPackage}/bin/hyprctl -i "$i" reload config-only
done
fi
)
'';
};
onChange = lib.mkIf (cfg.package != null) reloadConfig;
}
);
}
{
"hypr/hyprland.lua" = lib.mkIf (cfg.configType == "lua") (
let
pluginLoadCommands = map (entry: "hyprctl plugin load ${pluginPath entry}") cfg.plugins;
startupCommands =
lib.optionals cfg.systemd.enable [ systemdActivationCommand ] ++ pluginLoadCommands;
renderSettings =
let
names = lib.sort lib.lessThan (lib.attrNames cfg.settings);
renderArgs =
value:
if lib.isAttrs value && value ? _args then
lib.concatMapStringsSep ", " toLua value._args
else
toLua value;
importantNames = lib.unique (
lib.concatMap (
prefix: builtins.filter (name: lib.hasPrefix prefix name) names
) cfg.importantPrefixes
);
orderedNames = importantNames ++ builtins.filter (name: !(builtins.elem name importantNames)) names;
renderCall = name: value: "hl.${name}(${renderArgs value})\n";
renderCalls =
name: value:
lib.concatMapStrings (renderCall name) (if builtins.isList value then value else [ value ]);
in
lib.concatMapStrings (name: renderCalls name cfg.settings.${name}) orderedNames;
renderStartHook =
if startupCommands == [ ] then
""
else
''
hl.on("hyprland.start", function()
${lib.concatMapStrings (command: " hl.exec_cmd(${toLua command})\n") startupCommands}end)
'';
shouldGenerate =
cfg.systemd.enable || cfg.extraConfig != "" || cfg.settings != { } || cfg.plugins != [ ];
in
lib.mkIf shouldGenerate {
text = ''
-- Generated by Home Manager.
-- See https://wiki.hypr.land/Configuring/Start/
''
+ renderSettings
+ renderStartHook
+ lib.optionalString (cfg.extraConfig != "") cfg.extraConfig;
onChange = lib.mkIf (cfg.package != null) reloadConfig;
}
);
}
];
xdg.portal = {
enable = cfg.finalPortalPackage != null;

View file

@ -0,0 +1,18 @@
{
home.stateVersion = "26.05";
wayland.windowManager.hyprland = {
enable = true;
package = null;
portalPackage = null;
systemd.enable = false;
settings = {
config.input.kb_layout = "us";
};
};
nmt.script = ''
assertFileExists home-files/.config/hypr/hyprland.lua
assertPathNotExists home-files/.config/hypr/hyprland.conf
'';
}

View file

@ -0,0 +1,18 @@
{
home.stateVersion = "25.11";
wayland.windowManager.hyprland = {
enable = true;
package = null;
portalPackage = null;
systemd.enable = false;
settings = {
"$mod" = "SUPER";
};
};
nmt.script = ''
assertFileExists home-files/.config/hypr/hyprland.conf
assertPathNotExists home-files/.config/hypr/hyprland.lua
'';
}

View file

@ -0,0 +1,19 @@
{
home.stateVersion = "26.05";
wayland.windowManager.hyprland = {
enable = true;
configType = "hyprlang";
package = null;
portalPackage = null;
systemd.enable = false;
settings = {
"$mod" = "SUPER";
};
};
nmt.script = ''
assertFileExists home-files/.config/hypr/hyprland.conf
assertPathNotExists home-files/.config/hypr/hyprland.lua
'';
}

View file

@ -0,0 +1,19 @@
{
home.stateVersion = "25.11";
wayland.windowManager.hyprland = {
enable = true;
configType = "lua";
package = null;
portalPackage = null;
systemd.enable = false;
settings = {
config.input.kb_layout = "us";
};
};
nmt.script = ''
assertFileExists home-files/.config/hypr/hyprland.lua
assertPathNotExists home-files/.config/hypr/hyprland.conf
'';
}

View file

@ -2,6 +2,10 @@
lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux {
hyprland-simple-config = ./simple-config.nix;
hyprland-config-type-default-current = ./config-type-default-current.nix;
hyprland-config-type-default-legacy = ./config-type-default-legacy.nix;
hyprland-config-type-explicit-hyprlang-current = ./config-type-explicit-hyprlang-current.nix;
hyprland-config-type-explicit-lua-legacy = ./config-type-explicit-lua-legacy.nix;
hyprland-multiple-devices-config = ./multiple-devices-config.nix;
hyprland-null-all-packages-config = ./null-all-packages-config.nix;
hyprland-null-package-config = ./null-package-config.nix;
@ -10,4 +14,5 @@ lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux {
hyprland-inconsistent-config = ./inconsistent-config.nix;
hyprland-submaps-config = ./submaps-config.nix;
hyprland-submaps-on-dispatch = ./submaps-on-dispatch.nix;
hyprland-lua-config = ./lua-config.nix;
}

View file

@ -0,0 +1,106 @@
-- Generated by Home Manager.
-- See https://wiki.hypr.land/Configuring/Start/
hl.curve("smoothIn", {
["points"] = {
{
0.25,
1
},
{
0.5,
1
}
},
["type"] = "bezier"
})
hl.animation({
["bezier"] = "smoothIn",
["enabled"] = true,
["leaf"] = "border",
["speed"] = 2
})
hl.animation({
["bezier"] = "smoothIn",
["enabled"] = true,
["leaf"] = "windows",
["speed"] = 3,
["style"] = "popin 80%"
})
hl.bind("SUPER + Q", (hl.dsp.window.close()), {
["locked"] = true
})
hl.bind("SUPER + RETURN", (hl.dsp.exec_cmd("kitty")))
hl.config({
["ecosystem"] = {
["enforce_permissions"] = true
},
["input"] = {
["kb_layout"] = "ro",
["touchpad"] = {
["scroll_factor"] = 0.3
}
}
})
hl.device({
["enabled"] = true,
["name"] = "some:device"
})
hl.env("QT_QPA_PLATFORMTHEME", "qt5ct")
hl.env("XCURSOR_SIZE", "24")
hl.exec_cmd("hyprctl setcursor Bibata 24")
hl.gesture({
["action"] = "workspace",
["direction"] = "left",
["fingers"] = 3
})
hl.layer_rule({
["blur"] = false,
["match"] = {
["namespace"] = "waybar"
}
})
hl.monitor({
["mode"] = "highres",
["output"] = "desc:Monitor",
["position"] = "auto-right",
["scale"] = 1,
["vrr"] = 1
})
hl.on("hyprland.start", (function()
hl.exec_cmd("waybar")
end
))
hl.permission({
["binary"] = "^org\\.example\\..*",
["mode"] = "deny",
["type"] = "screencopy"
})
hl.window_rule({
["border_size"] = 2,
["match"] = {
["class"] = "kitty"
}
})
hl.workspace_rule({
["monitor"] = "DP-1",
["workspace"] = "1"
})
hl.on("hyprland.start", function()
hl.exec_cmd("@dbus@/bin/dbus-update-activation-environment --systemd DISPLAY HYPRLAND_INSTANCE_SIGNATURE WAYLAND_DISPLAY XDG_CURRENT_DESKTOP XDG_SESSION_TYPE && systemctl --user stop hyprland-session.target && systemctl --user start hyprland-session.target")
hl.exec_cmd("hyprctl plugin load /path/to/plugin1")
hl.exec_cmd("hyprctl plugin load /nix/store/00000000000000000000000000000000-foo/lib/libfoo.so")
end)
local mainMod = "SUPER"
local terminal = "kitty"
hl.define_submap("resize", function()
hl.bind("right", hl.dsp.window.move({ direction = "right" }))
end)
hl.on("hyprland.start", function()
hl.exec_cmd("waybar")
end)
hl.config({
decoration = {
rounding = 4,
},
})

View file

@ -0,0 +1,171 @@
{ config, lib, ... }:
{
wayland.windowManager.hyprland = {
enable = true;
configType = "lua";
package = null;
portalPackage = null;
plugins = [
"/path/to/plugin1"
(config.lib.test.mkStubPackage { name = "foo"; })
];
settings = {
config = {
input = {
kb_layout = "ro";
touchpad = {
scroll_factor = 0.3;
};
};
ecosystem.enforce_permissions = true;
};
monitor = {
output = "desc:Monitor";
mode = "highres";
position = "auto-right";
scale = 1;
vrr = 1;
};
device = {
name = "some:device";
enabled = true;
};
curve = {
_args = [
"smoothIn"
{
type = "bezier";
points = [
[
0.25
1
]
[
0.5
1
]
];
}
];
};
animation = [
{
leaf = "border";
enabled = true;
speed = 2;
bezier = "smoothIn";
}
{
leaf = "windows";
enabled = true;
speed = 3;
bezier = "smoothIn";
style = "popin 80%";
}
];
window_rule = {
match.class = "kitty";
border_size = 2;
};
layer_rule = {
match.namespace = "waybar";
blur = false;
};
workspace_rule = {
workspace = "1";
monitor = "DP-1";
};
permission = {
binary = "^org\\.example\\..*";
type = "screencopy";
mode = "deny";
};
gesture = {
fingers = 3;
direction = "left";
action = "workspace";
};
exec_cmd = [ "hyprctl setcursor Bibata 24" ];
env = [
{
_args = [
"QT_QPA_PLATFORMTHEME"
"qt5ct"
];
}
{
_args = [
"XCURSOR_SIZE"
"24"
];
}
];
bind = [
{
_args = [
"SUPER + Q"
(lib.generators.mkLuaInline "hl.dsp.window.close()")
{ locked = true; }
];
}
{
_args = [
"SUPER + RETURN"
(lib.generators.mkLuaInline ''hl.dsp.exec_cmd("kitty")'')
];
}
];
on = {
_args = [
"hyprland.start"
(lib.generators.mkLuaInline ''
function()
hl.exec_cmd("waybar")
end
'')
];
};
};
extraConfig = ''
local mainMod = "SUPER"
local terminal = "kitty"
hl.define_submap("resize", function()
hl.bind("right", hl.dsp.window.move({ direction = "right" }))
end)
hl.on("hyprland.start", function()
hl.exec_cmd("waybar")
end)
hl.config({
decoration = {
rounding = 4,
},
})
'';
};
nmt.script = ''
config=home-files/.config/hypr/hyprland.lua
assertFileExists "$config"
assertPathNotExists home-files/.config/hypr/hyprland.conf
normalizedConfig=$(normalizeStorePaths "$config")
assertFileContent "$normalizedConfig" ${./lua-config.lua}
'';
}