diff --git a/nix/lib.nix b/nix/lib.nix new file mode 100644 index 000000000..cb5223b97 --- /dev/null +++ b/nix/lib.nix @@ -0,0 +1,201 @@ +lib: let + inherit (lib) + attrNames + filterAttrs + foldl + generators + partition + ; + + inherit (lib.strings) + concatMapStrings + hasPrefix + ; + + /** + Convert a structured Nix attribute set into Hyprland's configuration format. + + This function takes a nested attribute set and converts it into Hyprland-compatible + configuration syntax, supporting top, bottom, and regular command sections. + + Commands are flattened using the `flattenAttrs` function, and attributes are formatted as + `key = value` pairs. Lists are expanded as duplicate keys to match Hyprland's expected format. + + Configuration: + + * `topCommandsPrefixes` - A list of prefixes to define **top** commands (default: `["$"]`). + * `bottomCommandsPrefixes` - A list of prefixes to define **bottom** commands (default: `[]`). + + Attention: + + - The function ensures top commands appear **first** and bottom commands **last**. + - The generated configuration is a **single string**, suitable for writing to a config file. + - Lists are converted into multiple entries, ensuring compatibility with Hyprland. + + # Inputs + + Structured function argument: + + : topCommandsPrefixes (optional, default: `["$"]`) + : A list of prefixes that define **top** commands. Any key starting with one of these + prefixes will be placed at the beginning of the configuration. + : bottomCommandsPrefixes (optional, default: `[]`) + : A list of prefixes that define **bottom** commands. Any key starting with one of these + prefixes will be placed at the end of the configuration. + + Value: + + : The attribute set to be converted to Hyprland configuration format. + + # Type + + ``` + toHyprlang :: AttrSet -> AttrSet -> String + ``` + + # Examples + :::{.example} + + ```nix + let + config = { + "$mod" = "SUPER"; + monitor = { + "HDMI-A-1" = "1920x1080@60,0x0,1"; + }; + exec = [ + "waybar" + "dunst" + ]; + }; + in lib.toHyprlang {} config + ``` + + **Output:** + ```nix + "$mod = SUPER" + "monitor:HDMI-A-1 = 1920x1080@60,0x0,1" + "exec = waybar" + "exec = dunst" + ``` + + ::: + */ + toHyprlang = { + topCommandsPrefixes ? ["$"], + bottomCommandsPrefixes ? [], + }: attrs: let + toHyprlang' = attrs: let + # Specially configured `toKeyValue` generator with support for duplicate keys + # and a legible key-value separator. + mkCommands = generators.toKeyValue { + mkKeyValue = generators.mkKeyValueDefault {} " = "; + listsAsDuplicateKeys = true; + indent = ""; # No indent, since we don't have nesting + }; + + # Flatten the attrset, combining keys in a "path" like `"a:b:c" = "x"`. + # Uses `flattenAttrs` with a colon separator. + commands = flattenAttrs (p: k: "${p}:${k}") attrs; + + # General filtering function to check if a key starts with any prefix in a given list. + filterCommands = list: n: + foldl (acc: prefix: acc || hasPrefix prefix n) false list; + + # Partition keys into top commands and the rest + result = partition (filterCommands topCommandsPrefixes) (attrNames commands); + topCommands = filterAttrs (n: _: builtins.elem n result.right) commands; + remainingCommands = removeAttrs commands result.right; + + # Partition remaining commands into bottom commands and regular commands + result2 = partition (filterCommands bottomCommandsPrefixes) result.wrong; + bottomCommands = filterAttrs (n: _: builtins.elem n result2.right) remainingCommands; + regularCommands = removeAttrs remainingCommands result2.right; + in + # Concatenate strings from mapping `mkCommands` over top, regular, and bottom commands. + concatMapStrings mkCommands [ + topCommands + regularCommands + bottomCommands + ]; + in + toHyprlang' attrs; + + /** + Flatten a nested attribute set into a flat attribute set, using a custom key separator function. + + This function recursively traverses a nested attribute set and produces a flat attribute set + where keys are joined using a user-defined function (`pred`). It allows transforming deeply + nested structures into a single-level attribute set while preserving key-value relationships. + + Configuration: + + * `pred` - A function `(string -> string -> string)` defining how keys should be concatenated. + + # Inputs + + Structured function argument: + + : pred (required) + : A function that determines how parent and child keys should be combined into a single key. + It takes a `prefix` (parent key) and `key` (current key) and returns the joined key. + + Value: + + : The nested attribute set to be flattened. + + # Type + + ``` + flattenAttrs :: (String -> String -> String) -> AttrSet -> AttrSet + ``` + + # Examples + :::{.example} + + ```nix + let + nested = { + a = "3"; + b = { c = "4"; d = "5"; }; + }; + + separator = (prefix: key: "${prefix}.${key}"); # Use dot notation + in lib.flattenAttrs separator nested + ``` + + **Output:** + ```nix + { + "a" = "3"; + "b.c" = "4"; + "b.d" = "5"; + } + ``` + + ::: + + */ + flattenAttrs = pred: attrs: let + flattenAttrs' = prefix: attrs: + builtins.foldl' ( + acc: key: let + value = attrs.${key}; + newKey = + if prefix == "" + then key + else pred prefix key; + in + acc + // ( + if builtins.isAttrs value + then flattenAttrs' newKey value + else {"${newKey}" = value;} + ) + ) {} (builtins.attrNames attrs); + in + flattenAttrs' "" attrs; +in +{ + inherit flattenAttrs toHyprlang; +} diff --git a/nix/module.nix b/nix/module.nix index 46191dfa8..0b8c4f420 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -5,72 +5,8 @@ inputs: { ... }: let inherit (pkgs.stdenv.hostPlatform) system; + selflib = import ./lib.nix lib; cfg = config.programs.hyprland; - - # basically 1:1 taken from https://github.com/nix-community/home-manager/blob/master/modules/services/window-managers/hyprland.nix - toHyprconf = { - attrs, - indentLevel ? 0, - importantPrefixes ? ["$"], - }: let - inherit - (lib) - all - concatMapStringsSep - concatStrings - concatStringsSep - filterAttrs - foldl - generators - hasPrefix - isAttrs - isList - mapAttrsToList - replicate - ; - - initialIndent = concatStrings (replicate indentLevel " "); - - toHyprconf' = indent: attrs: let - sections = - filterAttrs (n: v: isAttrs v || (isList v && all isAttrs v)) attrs; - - mkSection = n: attrs: - if lib.isList attrs - then (concatMapStringsSep "\n" (a: mkSection n a) attrs) - else '' - ${indent}${n} { - ${toHyprconf' " ${indent}" attrs}${indent}} - ''; - - mkFields = generators.toKeyValue { - listsAsDuplicateKeys = true; - inherit indent; - }; - - allFields = - filterAttrs (n: v: !(isAttrs v || (isList v && all isAttrs v))) - attrs; - - isImportantField = n: _: - foldl (acc: prev: - if hasPrefix prev n - then true - else acc) - false - importantPrefixes; - - importantFields = filterAttrs isImportantField allFields; - - fields = - builtins.removeAttrs allFields - (mapAttrsToList (n: _: n) importantFields); - in - mkFields importantFields - + concatStringsSep "\n" (mapAttrsToList mkSection sections) - + mkFields fields; - in - toHyprconf' initialIndent attrs; in { options = { programs.hyprland = { @@ -106,6 +42,9 @@ in { should be written as lists. Variables' and colors' names should be quoted. See for more examples. + Special categories (e.g `devices`) should be written as + `"devices[device-name]"`. + ::: {.note} Use the [](#programs.hyprland.plugins) option to declare plugins. @@ -151,20 +90,21 @@ in { ''; }; - sourceFirst = - lib.mkEnableOption '' - putting source entries at the top of the configuration - '' - // { - default = true; - }; - - importantPrefixes = lib.mkOption { + topPrefixes = lib.mkOption { type = with lib.types; listOf str; - default = ["$" "bezier" "name"] ++ lib.optionals cfg.sourceFirst ["source"]; - example = ["$" "bezier"]; + default = ["$" "bezier"]; + example = ["$" "bezier" "source"]; description = '' - List of prefix of attributes to source at the top of the config. + List of prefix of attributes to put at the top of the config. + ''; + }; + + bottomPrefixes = lib.mkOption { + type = with lib.types; listOf str; + default = []; + example = ["source"]; + description = '' + List of prefix of attributes to put at the bottom of the config. ''; }; }; @@ -173,38 +113,38 @@ in { { programs.hyprland = { package = lib.mkDefault inputs.self.packages.${system}.hyprland; - portalPackage = lib.mkDefault (inputs.self.packages.${system}.xdg-desktop-portal-hyprland.override { - hyprland = cfg.finalPackage; - }); + portalPackage = lib.mkDefault inputs.self.packages.${system}.xdg-desktop-portal-hyprland; }; } (lib.mkIf cfg.enable { environment.etc."xdg/hypr/hyprland.conf" = let shouldGenerate = cfg.extraConfig != "" || cfg.settings != {} || cfg.plugins != []; - pluginsToHyprconf = plugins: - toHyprconf { - attrs = { - plugin = let - mkEntry = entry: - if lib.types.package.check entry - then "${entry}/lib/lib${entry.pname}.so" - else entry; - in - map mkEntry cfg.plugins; - }; - inherit (cfg) importantPrefixes; + pluginsToHyprlang = plugins: + selflib.toHyprlang { + topCommandsPrefixes = cfg.topPrefixes; + bottomCommandsPrefixes = cfg.bottomPrefixes; + } + { + plugin = let + mkEntry = entry: + if lib.types.package.check entry + then "${entry}/lib/lib${entry.pname}.so" + else entry; + in + map mkEntry cfg.plugins; }; in lib.mkIf shouldGenerate { text = lib.optionalString (cfg.plugins != []) - (pluginsToHyprconf cfg.plugins) + (pluginsToHyprlang cfg.plugins) + lib.optionalString (cfg.settings != {}) - (toHyprconf { - attrs = cfg.settings; - inherit (cfg) importantPrefixes; - }) + (selflib.toHyprlang { + topCommandsPrefixes = cfg.topPrefixes; + bottomCommandsPrefixes = cfg.bottomPrefixes; + } + cfg.settings) + lib.optionalString (cfg.extraConfig != "") cfg.extraConfig; }; })