hyprland: add extraLuaFiles option

Allow Lua configs to be split across managed files under XDG_CONFIG_HOME/hypr.

Treat extraLuaFiles attribute names as Lua module names, so dotted names such as lib.helpers write lib/helpers.lua while autoloading with require("lib.helpers").

Add assertions for invalid configType usage, generated hyprland.lua collisions, and duplicate resolved Lua file targets.
This commit is contained in:
Austin Horstman
2026-05-14 10:15:57 -05:00
parent 5148001968
commit a6a13bb0a0
11 changed files with 234 additions and 2 deletions

View File

@@ -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(...)`.
'';
}

View File

@@ -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 = {

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -0,0 +1,3 @@
hl.on("hyprland.start", function()
hl.exec_cmd("waybar")
end)

View File

@@ -0,0 +1,3 @@
local M = {}
M.mainMod = "SUPER"
return M

View File

@@ -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;
}

View File

@@ -0,0 +1,2 @@
local vars = require("00-vars")
hl.bind(vars.mainMod .. " + RETURN", hl.dsp.exec_cmd("kitty"))

View File

@@ -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")

View File

@@ -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}
'';
}

View File

@@ -0,0 +1,3 @@
local M = {}
M.terminal = "kitty"
return M