Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nix/module: toHyprconf -> toHyprlang #9221

Merged
merged 6 commits into from
Mar 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 201 additions & 0 deletions nix/lib.nix
Original file line number Diff line number Diff line change
@@ -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;
}
132 changes: 36 additions & 96 deletions nix/module.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -106,6 +42,9 @@ in {
should be written as lists. Variables' and colors' names should be
quoted. See <https://wiki.hyprland.org> 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.
Expand Down Expand Up @@ -151,20 +90,21 @@ in {
'';
};

sourceFirst =
lib.mkEnableOption ''
putting source entries at the top of the configuration
''
// {
default = true;
};
topPrefixes = lib.mkOption {
type = with lib.types; listOf str;
default = ["$" "bezier"];
example = ["$" "bezier" "source"];
description = ''
List of prefix of attributes to put at the top of the config.
'';
};

importantPrefixes = lib.mkOption {
bottomPrefixes = lib.mkOption {
type = with lib.types; listOf str;
default = ["$" "bezier" "name"] ++ lib.optionals cfg.sourceFirst ["source"];
example = ["$" "bezier"];
default = [];
example = ["source"];
description = ''
List of prefix of attributes to source at the top of the config.
List of prefix of attributes to put at the bottom of the config.
'';
};
};
Expand All @@ -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;
};
})
Expand Down
Loading