diff --git a/modules/misc/news/2026/05/2026-05-14_00-00-00.nix b/modules/misc/news/2026/05/2026-05-14_00-00-00.nix new file mode 100644 index 000000000..b255fddab --- /dev/null +++ b/modules/misc/news/2026/05/2026-05-14_00-00-00.nix @@ -0,0 +1,15 @@ +{ config, ... }: + +{ + time = "2026-05-14T00:00:00+00:00"; + condition = config.wayland.windowManager.hyprland.enable; + message = '' + A new option `wayland.windowManager.hyprland.extraLuaFiles` is available for + managing additional Lua files under `$XDG_CONFIG_HOME/hypr` when using + `wayland.windowManager.hyprland.configType = "lua"`. + + Files can be provided as paths or strings. Attribute names are treated as + Lua module names, so `lib.helpers` writes `lib/helpers.lua`, and can be + automatically loaded from the generated `hyprland.lua` with `require(...)`. + ''; +} diff --git a/modules/services/window-managers/hyprland.nix b/modules/services/window-managers/hyprland.nix index 7f48d7cbf..45b23453f 100644 --- a/modules/services/window-managers/hyprland.nix +++ b/modules/services/window-managers/hyprland.nix @@ -31,6 +31,10 @@ let pluginPath = entry: if lib.types.package.check entry then "${entry}/lib/lib${entry.pname}.so" else entry; + luaModuleName = name: lib.replaceStrings [ "/" ] [ "." ] (lib.removeSuffix ".lua" name); + + luaFileName = name: "${lib.replaceStrings [ "." ] [ "/" ] (luaModuleName name)}.lua"; + reloadConfig = '' ( XDG_RUNTIME_DIR=''${XDG_RUNTIME_DIR:-/run/user/$(id -u)} @@ -393,6 +397,75 @@ in ''; }; + extraLuaFiles = lib.mkOption { + type = + with lib.types; + attrsOf ( + coercedTo (either path lines) + (content: { + inherit content; + autoLoad = true; + }) + (submodule { + options = { + content = lib.mkOption { + type = either path lines; + description = '' + Lua file content, set either by specifying a path to a Lua + file or by providing a multi-line Lua string. + ''; + }; + + autoLoad = lib.mkOption { + type = bool; + default = true; + description = '' + Whether to generate a `require(...)` call for this file in + {file}`$XDG_CONFIG_HOME/hypr/hyprland.lua`. + ''; + }; + }; + }) + ); + default = { }; + description = '' + Extra Lua files written under {file}`$XDG_CONFIG_HOME/hypr`. + + Attribute names are used as Lua module names and converted to file + names with a {file}`.lua` suffix added when missing. For example, + `bindings` writes + {file}`$XDG_CONFIG_HOME/hypr/bindings.lua`, while + `lib.helpers` writes {file}`$XDG_CONFIG_HOME/hypr/lib/helpers.lua`. + + Files with {option}`autoLoad` enabled generate `require(...)` calls in + {file}`$XDG_CONFIG_HOME/hypr/hyprland.lua` after adding the Hypr config + directory to Lua's `package.path`. Use {option}`autoLoad = false` for + helper modules that are imported by other Lua files. + + This option only affects generated files when + {option}`wayland.windowManager.hyprland.configType` is `"lua"`. + ''; + example = lib.literalExpression '' + { + "00-vars" = '\' + local M = {} + M.mainMod = "SUPER" + return M + '\'; + + "bindings" = { + content = ./bindings.lua; + autoLoad = true; + }; + + "lib.helpers" = { + content = ./helpers.lua; + autoLoad = false; + }; + } + ''; + }; + sourceFirst = lib.mkEnableOption '' putting source entries at the top of the configuration @@ -427,6 +500,18 @@ in assertion = !builtins.hasAttr "reset" cfg.submaps; message = "Submaps can't be named 'reset'. The name 'reset' is reserved in order to have a way to switch to the default submap; as if 'reset' was its name."; } + { + assertion = !builtins.elem "hyprland.lua" (map luaFileName (lib.attrNames cfg.extraLuaFiles)); + message = "wayland.windowManager.hyprland.extraLuaFiles cannot define hyprland.lua because it is generated by the Hyprland module."; + } + { + assertion = + let + targets = map luaFileName (lib.attrNames cfg.extraLuaFiles); + in + lib.length targets == lib.length (lib.unique targets); + message = "wayland.windowManager.hyprland.extraLuaFiles contains entries that resolve to the same Lua file path."; + } ]; warnings = @@ -434,9 +519,10 @@ in inconsistent = (cfg.systemd.enable || cfg.plugins != [ ]) && cfg.extraConfig == "" + && cfg.extraLuaFiles == { } && cfg.settings == { } && cfg.submaps == { }; - warning = "You have enabled hyprland.systemd.enable or listed plugins in hyprland.plugins but do not have any configuration in hyprland.settings, hyprland.extraConfig or hyprland.submaps. This is almost certainly a mistake."; + warning = "You have enabled hyprland.systemd.enable or listed plugins in hyprland.plugins but do not have any configuration in hyprland.settings, hyprland.extraConfig, hyprland.extraLuaFiles or hyprland.submaps. This is almost certainly a mistake."; filterNonBinds = attrs: builtins.filter (n: builtins.match "bind[[:lower:]]*" n == null) (builtins.attrNames attrs); @@ -589,6 +675,22 @@ in ${lib.concatMapStrings (command: " hl.exec_cmd(${toLua command})\n") startupCommands}end) ''; + renderLuaFiles = + let + autoloadFiles = lib.filterAttrs (_: file: file.autoLoad) cfg.extraLuaFiles; + names = lib.sort lib.lessThan (lib.attrNames autoloadFiles); + in + if names == [ ] then + "" + else + renderSection "extraLuaFiles" ( + '' + local hm_xdg_config_home = os.getenv("XDG_CONFIG_HOME") or ${toLua config.xdg.configHome} + package.path = hm_xdg_config_home .. "/hypr/?.lua;" .. hm_xdg_config_home .. "/hypr/?/init.lua;" .. package.path + '' + + lib.concatMapStringsSep "\n" (name: "require(${toLua (luaModuleName name)})") names + ); + renderSubmaps = let renderLuaArg = value: lib.replaceStrings [ "\n" ] [ "\n " ] (renderArgs value); @@ -623,6 +725,7 @@ in shouldGenerate = cfg.systemd.enable || cfg.extraConfig != "" + || cfg.extraLuaFiles != { } || cfg.settings != { } || cfg.plugins != [ ] || hasLuaSubmaps; @@ -633,6 +736,7 @@ in -- See https://wiki.hypr.land/Configuring/Start/ '' + + renderLuaFiles + renderSettings + renderSubmaps + renderStartHook @@ -642,6 +746,17 @@ in } ); } + (lib.mkIf (cfg.configType == "lua") ( + lib.mapAttrs' ( + name: file: + lib.nameValuePair "hypr/${luaFileName name}" ( + if builtins.isPath file.content || lib.isStorePath file.content then + { source = file.content; } + else + { text = file.content; } + ) + ) cfg.extraLuaFiles + )) ]; xdg.portal = { diff --git a/tests/modules/services/hyprland/default.nix b/tests/modules/services/hyprland/default.nix index fc65e012a..e95073f1b 100644 --- a/tests/modules/services/hyprland/default.nix +++ b/tests/modules/services/hyprland/default.nix @@ -15,4 +15,6 @@ lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux { hyprland-submaps-config = ./submaps-config.nix; hyprland-submaps-on-dispatch = ./submaps-on-dispatch.nix; hyprland-lua-config = ./lua-config.nix; + hyprland-lua-files-assertions = ./lua-files-assertions.nix; + hyprland-lua-files-config = ./lua-files-config.nix; } diff --git a/tests/modules/services/hyprland/inconsistent-config.nix b/tests/modules/services/hyprland/inconsistent-config.nix index 0e76513d5..f1de44a71 100644 --- a/tests/modules/services/hyprland/inconsistent-config.nix +++ b/tests/modules/services/hyprland/inconsistent-config.nix @@ -9,7 +9,7 @@ }; test.asserts.warnings.expected = [ - "You have enabled hyprland.systemd.enable or listed plugins in hyprland.plugins but do not have any configuration in hyprland.settings, hyprland.extraConfig or hyprland.submaps. This is almost certainly a mistake." + "You have enabled hyprland.systemd.enable or listed plugins in hyprland.plugins but do not have any configuration in hyprland.settings, hyprland.extraConfig, hyprland.extraLuaFiles or hyprland.submaps. This is almost certainly a mistake." ]; test.asserts.warnings.enable = true; diff --git a/tests/modules/services/hyprland/lua-file-from-path.lua b/tests/modules/services/hyprland/lua-file-from-path.lua new file mode 100644 index 000000000..ace0f3c19 --- /dev/null +++ b/tests/modules/services/hyprland/lua-file-from-path.lua @@ -0,0 +1,3 @@ +hl.on("hyprland.start", function() + hl.exec_cmd("waybar") +end) diff --git a/tests/modules/services/hyprland/lua-files-00-vars.lua b/tests/modules/services/hyprland/lua-files-00-vars.lua new file mode 100644 index 000000000..852fc6e73 --- /dev/null +++ b/tests/modules/services/hyprland/lua-files-00-vars.lua @@ -0,0 +1,3 @@ +local M = {} +M.mainMod = "SUPER" +return M diff --git a/tests/modules/services/hyprland/lua-files-assertions.nix b/tests/modules/services/hyprland/lua-files-assertions.nix new file mode 100644 index 000000000..290605acf --- /dev/null +++ b/tests/modules/services/hyprland/lua-files-assertions.nix @@ -0,0 +1,21 @@ +{ + wayland.windowManager.hyprland = { + enable = true; + configType = "hyprlang"; + package = null; + portalPackage = null; + + extraLuaFiles = { + foo = ""; + "foo.lua" = ""; + hyprland = ""; + }; + }; + + test.asserts.assertions.expected = [ + "wayland.windowManager.hyprland.extraLuaFiles cannot define hyprland.lua because it is generated by the Hyprland module." + "wayland.windowManager.hyprland.extraLuaFiles contains entries that resolve to the same Lua file path." + ]; + + test.asserts.warnings.enable = false; +} diff --git a/tests/modules/services/hyprland/lua-files-bindings.lua b/tests/modules/services/hyprland/lua-files-bindings.lua new file mode 100644 index 000000000..221c78a9d --- /dev/null +++ b/tests/modules/services/hyprland/lua-files-bindings.lua @@ -0,0 +1,2 @@ +local vars = require("00-vars") +hl.bind(vars.mainMod .. " + RETURN", hl.dsp.exec_cmd("kitty")) diff --git a/tests/modules/services/hyprland/lua-files-config.lua b/tests/modules/services/hyprland/lua-files-config.lua new file mode 100644 index 000000000..933a403eb --- /dev/null +++ b/tests/modules/services/hyprland/lua-files-config.lua @@ -0,0 +1,9 @@ +-- Generated by Home Manager. +-- See https://wiki.hypr.land/Configuring/Start/ + +-- extraLuaFiles +local hm_xdg_config_home = os.getenv("XDG_CONFIG_HOME") or "/home/hm-user/.config" +package.path = hm_xdg_config_home .. "/hypr/?.lua;" .. hm_xdg_config_home .. "/hypr/?/init.lua;" .. package.path +require("00-vars") +require("from-path") +require("ui.bindings") diff --git a/tests/modules/services/hyprland/lua-files-config.nix b/tests/modules/services/hyprland/lua-files-config.nix new file mode 100644 index 000000000..e72abc0a3 --- /dev/null +++ b/tests/modules/services/hyprland/lua-files-config.nix @@ -0,0 +1,59 @@ +_: + +{ + wayland.windowManager.hyprland = { + enable = true; + configType = "lua"; + package = null; + portalPackage = null; + + systemd.enable = false; + + extraLuaFiles = { + "00-vars" = '' + local M = {} + M.mainMod = "SUPER" + return M + ''; + + "ui.bindings" = { + content = '' + local vars = require("00-vars") + hl.bind(vars.mainMod .. " + RETURN", hl.dsp.exec_cmd("kitty")) + ''; + }; + + "lib.helpers" = { + content = '' + local M = {} + M.terminal = "kitty" + return M + ''; + autoLoad = false; + }; + + "from-path.lua" = ./lua-file-from-path.lua; + }; + }; + + nmt.script = '' + config=home-files/.config/hypr/hyprland.lua + assertFileExists "$config" + assertPathNotExists home-files/.config/hypr/hyprland.conf + assertPathNotExists home-files/.config/hypr/.luarc.json + + assertFileContent "$config" ${./lua-files-config.lua} + + assertFileContent home-files/.config/hypr/00-vars.lua \ + ${./lua-files-00-vars.lua} + + assertFileContent home-files/.config/hypr/ui/bindings.lua \ + ${./lua-files-bindings.lua} + + assertFileContent home-files/.config/hypr/lib/helpers.lua \ + ${./lua-files-helpers.lua} + + assertFileContent home-files/.config/hypr/from-path.lua \ + ${./lua-file-from-path.lua} + ''; +} diff --git a/tests/modules/services/hyprland/lua-files-helpers.lua b/tests/modules/services/hyprland/lua-files-helpers.lua new file mode 100644 index 000000000..ebb9662d9 --- /dev/null +++ b/tests/modules/services/hyprland/lua-files-helpers.lua @@ -0,0 +1,3 @@ +local M = {} +M.terminal = "kitty" +return M