From 9773b435925f8239f3ed2fdbaaa006201de034fe Mon Sep 17 00:00:00 2001 From: Ananya Timalsina <84459091+ananyatimalsina@users.noreply.github.com> Date: Sun, 15 Feb 2026 12:35:34 +0100 Subject: [PATCH 1/4] refactor(nix): add structured config support to home-manager module Convert settings from raw text to structured Nix attrs, following Hyprland's module pattern. Implementation based 1:1 on Hyprland's design - all credit to the Hyprland project. - Add nix/lib.nix with toMango conversion function - Support nested attrs, lists for duplicate keys - Add extraConfig, topPrefixes, bottomPrefixes options - Auto-add exec-once for autostart.sh Adapted for mangowc syntax (underscore separators vs colons). --- nix/hm-modules.nix | 161 +++++++++++++++++++++++----- nix/lib.nix | 257 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 393 insertions(+), 25 deletions(-) create mode 100644 nix/lib.nix diff --git a/nix/hm-modules.nix b/nix/hm-modules.nix index 85d57908..2c450f61 100644 --- a/nix/hm-modules.nix +++ b/nix/hm-modules.nix @@ -1,18 +1,22 @@ -self: { +self: +{ lib, config, pkgs, ... -}: let +}: +let cfg = config.wayland.windowManager.mango; + selflib = import ./lib.nix lib; variables = lib.concatStringsSep " " cfg.systemd.variables; extraCommands = lib.concatStringsSep " && " cfg.systemd.extraCommands; - systemdActivation = ''${pkgs.dbus}/bin/dbus-update-activation-environment --systemd ${variables}; ${extraCommands}''; + systemdActivation = "${pkgs.dbus}/bin/dbus-update-activation-environment --systemd ${variables}; ${extraCommands}"; autostart_sh = pkgs.writeShellScript "autostart.sh" '' ${lib.optionalString cfg.systemd.enable systemdActivation} ${cfg.autostart_sh} ''; -in { +in +{ options = { wayland.windowManager.mango = with lib; { enable = mkOption { @@ -54,7 +58,7 @@ in { "XCURSOR_THEME" "XCURSOR_SIZE" ]; - example = ["--all"]; + example = [ "--all" ]; description = '' Environment variables imported into the systemd and D-Bus user environment. ''; @@ -75,32 +79,140 @@ in { ''; }; settings = mkOption { - description = "mango config content"; - type = types.lines; - default = ""; - example = '' - # menu and terminal - bind=Alt,space,spawn,rofi -show drun - bind=Alt,Return,spawn,foot + type = + with lib.types; + let + valueType = + nullOr (oneOf [ + bool + int + float + str + path + (attrsOf valueType) + (listOf valueType) + ]) + // { + description = "Mango configuration value"; + }; + in + valueType; + default = { }; + description = '' + Mango configuration written in Nix. Entries with the same key + should be written as lists. Variables and colors names should be + quoted. See for more examples. + + ::: {.note} + This option uses a structured format that is converted to Mango's + configuration syntax. Nested attributes are flattened with underscore separators. + For example: `animation.duration_open = 400` becomes `animation_duration_open = 400` + ::: + ''; + example = lib.literalExpression '' + { + # Window effects + blur = 1; + blur_optimized = 1; + blur_params = { + radius = 5; + num_passes = 2; + }; + border_radius = 6; + focused_opacity = 1.0; + + # Animations - use underscores for multi-part keys + animations = 1; + animation_type_open = "slide"; + animation_type_close = "slide"; + animation_duration_open = 400; + animation_duration_close = 800; + + # Or use nested attrs (will be flattened with underscores) + animation_curve = { + open = "0.46,1.0,0.29,1"; + close = "0.08,0.92,0,1"; + }; + + # Use lists for duplicate keys like bind and tagrule + bind = [ + "SUPER,r,reload_config" + "Alt,space,spawn,rofi -show drun" + "Alt,Return,spawn,foot" + ]; + + tagrule = [ + "id:1,layout_name:tile" + "id:2,layout_name:scroller" + ]; + } ''; }; + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra configuration lines to add to `~/.config/mango/config.conf`. + This is useful for advanced configurations that don't fit the structured + settings format, or for options that aren't yet supported by the module. + ''; + example = '' + # Advanced config that doesn't fit structured format + special_option = 1 + ''; + }; + topPrefixes = mkOption { + type = with lib.types; listOf str; + default = [ ]; + description = '' + List of prefixes for attributes that should appear at the top of the config file. + Attributes starting with these prefixes will be sorted to the beginning. + ''; + example = [ "source" ]; + }; + bottomPrefixes = mkOption { + type = with lib.types; listOf str; + default = [ ]; + description = '' + List of prefixes for attributes that should appear at the bottom of the config file. + Attributes starting with these prefixes will be sorted to the end. + ''; + example = [ "source" ]; + }; autostart_sh = mkOption { - description = "WARRNING: This is a shell script, but no need to add shebang"; + description = '' + Shell script to run on mango startup. No shebang needed. + + When this option is set, the script will be written to + `~/.config/mango/autostart.sh` and an `exec-once` line + will be automatically added to the config to execute it. + ''; type = types.lines; default = ""; example = '' waybar & + dunst & ''; }; }; }; config = lib.mkIf cfg.enable { - home.packages = [cfg.package]; + home.packages = [ cfg.package ]; xdg.configFile = { - "mango/config.conf" = lib.mkIf (cfg.settings != "") { - text = cfg.settings; - }; + "mango/config.conf" = + lib.mkIf (cfg.settings != { } || cfg.extraConfig != "" || cfg.autostart_sh != "") + { + text = + lib.optionalString (cfg.settings != { }) ( + selflib.toMango { + topCommandsPrefixes = cfg.topPrefixes; + bottomCommandsPrefixes = cfg.bottomPrefixes; + } cfg.settings + ) + + lib.optionalString (cfg.extraConfig != "") cfg.extraConfig + + lib.optionalString (cfg.autostart_sh != "") "\nexec-once=~/.config/mango/autostart.sh\n"; + }; "mango/autostart.sh" = lib.mkIf (cfg.autostart_sh != "") { source = autostart_sh; executable = true; @@ -109,14 +221,13 @@ in { systemd.user.targets.mango-session = lib.mkIf cfg.systemd.enable { Unit = { Description = "mango compositor session"; - Documentation = ["man:systemd.special(7)"]; - BindsTo = ["graphical-session.target"]; - Wants = - [ - "graphical-session-pre.target" - ] - ++ lib.optional cfg.systemd.xdgAutostart "xdg-desktop-autostart.target"; - After = ["graphical-session-pre.target"]; + Documentation = [ "man:systemd.special(7)" ]; + BindsTo = [ "graphical-session.target" ]; + Wants = [ + "graphical-session-pre.target" + ] + ++ lib.optional cfg.systemd.xdgAutostart "xdg-desktop-autostart.target"; + After = [ "graphical-session-pre.target" ]; Before = lib.optional cfg.systemd.xdgAutostart "xdg-desktop-autostart.target"; }; }; diff --git a/nix/lib.nix b/nix/lib.nix new file mode 100644 index 00000000..406d94ae --- /dev/null +++ b/nix/lib.nix @@ -0,0 +1,257 @@ +lib: +let + inherit (lib) + attrNames + filterAttrs + foldl + generators + partition + ; + + inherit (lib.strings) + concatMapStrings + hasPrefix + ; + + /** + Convert a structured Nix attribute set into Mango's configuration format. + + This function takes a nested attribute set and converts it into Mango-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 Mango'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 Mango. + + # 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 + + ``` + toMango :: AttrSet -> AttrSet -> String + ``` + + # Examples + :::{.example} + + ## Basic mangowc configuration + + ```nix + let + config = { + blur = 1; + blur_params_radius = 5; + border_radius = 6; + animations = 1; + animation_duration_open = 400; + }; + in lib.toMango {} config + ``` + + **Output:** + ``` + animations = 1 + animation_duration_open = 400 + blur = 1 + blur_params_radius = 5 + border_radius = 6 + ``` + + ## Using nested attributes + + ```nix + let + config = { + blur = 1; + blur_params = { + radius = 5; + num_passes = 2; + noise = 0.02; + }; + animation_curve = { + open = "0.46,1.0,0.29,1"; + close = "0.08,0.92,0,1"; + }; + }; + in lib.toMango {} config + ``` + + **Output:** + ``` + animation_curve_close = 0.08,0.92,0,1 + animation_curve_open = 0.46,1.0,0.29,1 + blur = 1 + blur_params_noise = 0.02 + blur_params_num_passes = 2 + blur_params_radius = 5 + ``` + + ## Using lists for duplicate keys + + ```nix + let + config = { + bind = [ + "SUPER,r,reload_config" + "Alt,space,spawn,rofi -show drun" + "Alt,Return,spawn,foot" + ]; + tagrule = [ + "id:1,layout_name:tile" + "id:2,layout_name:scroller" + ]; + }; + in lib.toMango {} config + ``` + + **Output:** + ``` + bind = SUPER,r,reload_config + bind = Alt,space,spawn,rofi -show drun + bind = Alt,Return,spawn,foot + tagrule = id:1,layout_name:tile + tagrule = id:2,layout_name:scroller + ``` + + ::: + */ + toMango = + { + topCommandsPrefixes ? [ ], + bottomCommandsPrefixes ? [ ], + }: + attrs: + let + toMango' = + 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 an underscore 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 + toMango' 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 toMango; +} From 3eb9fe7163f7974699c1ca17f865c0ffd17077ac Mon Sep 17 00:00:00 2001 From: Ananya Timalsina <84459091+ananyatimalsina@users.noreply.github.com> Date: Sun, 15 Feb 2026 14:04:56 +0100 Subject: [PATCH 2/4] feat(nix): add keymode support for modal keybindings --- nix/hm-modules.nix | 14 +++++++++++ nix/lib.nix | 59 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/nix/hm-modules.nix b/nix/hm-modules.nix index 2c450f61..81cec1e8 100644 --- a/nix/hm-modules.nix +++ b/nix/hm-modules.nix @@ -107,6 +107,9 @@ in This option uses a structured format that is converted to Mango's configuration syntax. Nested attributes are flattened with underscore separators. For example: `animation.duration_open = 400` becomes `animation_duration_open = 400` + + Keymodes (submaps) are supported via the special `keymode` attribute. Each keymode + is a nested attribute set under `keymode` that contains its own bindings. ::: ''; example = lib.literalExpression '' @@ -139,12 +142,23 @@ in "SUPER,r,reload_config" "Alt,space,spawn,rofi -show drun" "Alt,Return,spawn,foot" + "ALT,R,setkeymode,resize" # Enter resize mode ]; tagrule = [ "id:1,layout_name:tile" "id:2,layout_name:scroller" ]; + + # Keymodes (submaps) for modal keybindings + keymode = { + resize = { + bind = [ + "NONE,Left,resizewin,-10,0" + "NONE,Escape,setkeymode,default" + ]; + }; + }; } ''; }; diff --git a/nix/lib.nix b/nix/lib.nix index 406d94ae..9dfd2ff6 100644 --- a/nix/lib.nix +++ b/nix/lib.nix @@ -6,6 +6,7 @@ let foldl generators partition + removeAttrs ; inherit (lib.strings) @@ -136,6 +137,39 @@ let tagrule = id:2,layout_name:scroller ``` + ## Using keymodes (submaps) + + ```nix + let + config = { + bind = [ + "SUPER,Q,killclient" + "ALT,R,setkeymode,resize" + ]; + keymode = { + resize = { + bind = [ + "NONE,Left,resizewin,-10,0" + "NONE,Right,resizewin,10,0" + "NONE,Escape,setkeymode,default" + ]; + }; + }; + }; + in lib.toMango {} config + ``` + + **Output:** + ``` + bind = SUPER,Q,killclient + bind = ALT,R,setkeymode,resize + + keymode = resize + bind = NONE,Left,resizewin,-10,0 + bind = NONE,Right,resizewin,10,0 + bind = NONE,Escape,setkeymode,default + ``` + ::: */ toMango = @@ -156,9 +190,28 @@ let indent = ""; # No indent, since we don't have nesting }; + # Extract keymode definitions if they exist + keymodes = attrs.keymode or { }; + attrsWithoutKeymodes = removeAttrs attrs [ "keymode" ]; + + # Generate keymode blocks + # Format: keymode=name\nbind=...\nbind=...\n + mkKeymodeBlock = + name: modeAttrs: + let + modeCommands = flattenAttrs (p: k: "${p}_${k}") modeAttrs; + in + "keymode = ${name}\n${mkCommands modeCommands}"; + + keymodeBlocks = + if keymodes == { } then + "" + else + "\n" + concatMapStrings (name: mkKeymodeBlock name keymodes.${name} + "\n") (attrNames keymodes); + # Flatten the attrset, combining keys in a "path" like `"a_b_c" = "x"`. # Uses `flattenAttrs` with an underscore separator. - commands = flattenAttrs (p: k: "${p}_${k}") attrs; + commands = flattenAttrs (p: k: "${p}_${k}") attrsWithoutKeymodes; # 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; @@ -174,11 +227,13 @@ let regularCommands = removeAttrs remainingCommands result2.right; in # Concatenate strings from mapping `mkCommands` over top, regular, and bottom commands. + # Keymodes are appended at the end. concatMapStrings mkCommands [ topCommands regularCommands bottomCommands - ]; + ] + + keymodeBlocks; in toMango' attrs; From c9a93a3edb37b1c45b8a3b2b134bac871bba4759 Mon Sep 17 00:00:00 2001 From: Ananya Timalsina <84459091+ananyatimalsina@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:12:11 +0100 Subject: [PATCH 3/4] feat(nix): support old config format with deprecation warning --- nix/hm-modules.nix | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/nix/hm-modules.nix b/nix/hm-modules.nix index 81cec1e8..4d48ddf6 100644 --- a/nix/hm-modules.nix +++ b/nix/hm-modules.nix @@ -212,17 +212,31 @@ in }; config = lib.mkIf cfg.enable { + # Backwards compatibility warning for old string-based config + warnings = lib.optional (builtins.isString cfg.settings) '' + wayland.windowManager.mango.settings: Using a string for settings is deprecated. + Please migrate to the new structured attribute set format. + See the module documentation for examples, or use the 'extraConfig' option for raw config strings. + The old string format will be removed in a future release. + ''; + home.packages = [ cfg.package ]; xdg.configFile = { "mango/config.conf" = lib.mkIf (cfg.settings != { } || cfg.extraConfig != "" || cfg.autostart_sh != "") { text = - lib.optionalString (cfg.settings != { }) ( - selflib.toMango { - topCommandsPrefixes = cfg.topPrefixes; - bottomCommandsPrefixes = cfg.bottomPrefixes; - } cfg.settings + # Support old string-based config during transition period + ( + if builtins.isString cfg.settings then + cfg.settings + else + lib.optionalString (cfg.settings != { }) ( + selflib.toMango { + topCommandsPrefixes = cfg.topPrefixes; + bottomCommandsPrefixes = cfg.bottomPrefixes; + } cfg.settings + ) ) + lib.optionalString (cfg.extraConfig != "") cfg.extraConfig + lib.optionalString (cfg.autostart_sh != "") "\nexec-once=~/.config/mango/autostart.sh\n"; From 1f8f4cc2f8e621994db88b7df74c83edafada753 Mon Sep 17 00:00:00 2001 From: Ananya Timalsina <84459091+ananyatimalsina@users.noreply.github.com> Date: Sun, 1 Mar 2026 15:40:25 +0100 Subject: [PATCH 4/4] feat(nix): add build-time configuration validation Uses `pkgs.runCommand` in the home-manager module to parse and validate the generated config file prior to deployment, preventing broken setups. --- nix/hm-modules.nix | 102 +++++++++++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 46 deletions(-) diff --git a/nix/hm-modules.nix b/nix/hm-modules.nix index 4d48ddf6..f00d9c68 100644 --- a/nix/hm-modules.nix +++ b/nix/hm-modules.nix @@ -211,53 +211,63 @@ in }; }; - config = lib.mkIf cfg.enable { - # Backwards compatibility warning for old string-based config - warnings = lib.optional (builtins.isString cfg.settings) '' - wayland.windowManager.mango.settings: Using a string for settings is deprecated. - Please migrate to the new structured attribute set format. - See the module documentation for examples, or use the 'extraConfig' option for raw config strings. - The old string format will be removed in a future release. - ''; + config = lib.mkIf cfg.enable ( + let + finalConfigText = + # Support old string-based config during transition period + ( + if builtins.isString cfg.settings then + cfg.settings + else + lib.optionalString (cfg.settings != { }) ( + selflib.toMango { + topCommandsPrefixes = cfg.topPrefixes; + bottomCommandsPrefixes = cfg.bottomPrefixes; + } cfg.settings + ) + ) + + lib.optionalString (cfg.extraConfig != "") cfg.extraConfig + + lib.optionalString (cfg.autostart_sh != "") "\nexec-once=~/.config/mango/autostart.sh\n"; - home.packages = [ cfg.package ]; - xdg.configFile = { - "mango/config.conf" = - lib.mkIf (cfg.settings != { } || cfg.extraConfig != "" || cfg.autostart_sh != "") - { - text = - # Support old string-based config during transition period - ( - if builtins.isString cfg.settings then - cfg.settings - else - lib.optionalString (cfg.settings != { }) ( - selflib.toMango { - topCommandsPrefixes = cfg.topPrefixes; - bottomCommandsPrefixes = cfg.bottomPrefixes; - } cfg.settings - ) - ) - + lib.optionalString (cfg.extraConfig != "") cfg.extraConfig - + lib.optionalString (cfg.autostart_sh != "") "\nexec-once=~/.config/mango/autostart.sh\n"; - }; - "mango/autostart.sh" = lib.mkIf (cfg.autostart_sh != "") { - source = autostart_sh; - executable = true; + validatedConfig = pkgs.runCommand "mango-config.conf" { } '' + cp ${pkgs.writeText "mango-config.conf" finalConfigText} "$out" + ${cfg.package}/bin/mango -c "$out" -p || exit 1 + ''; + in + { + # Backwards compatibility warning for old string-based config + warnings = lib.optional (builtins.isString cfg.settings) '' + wayland.windowManager.mango.settings: Using a string for settings is deprecated. + Please migrate to the new structured attribute set format. + See the module documentation for examples, or use the 'extraConfig' option for raw config strings. + The old string format will be removed in a future release. + ''; + + home.packages = [ cfg.package ]; + xdg.configFile = { + "mango/config.conf" = + lib.mkIf (cfg.settings != { } || cfg.extraConfig != "" || cfg.autostart_sh != "") + { + source = validatedConfig; + }; + "mango/autostart.sh" = lib.mkIf (cfg.autostart_sh != "") { + source = autostart_sh; + executable = true; + }; }; - }; - systemd.user.targets.mango-session = lib.mkIf cfg.systemd.enable { - Unit = { - Description = "mango compositor session"; - Documentation = [ "man:systemd.special(7)" ]; - BindsTo = [ "graphical-session.target" ]; - Wants = [ - "graphical-session-pre.target" - ] - ++ lib.optional cfg.systemd.xdgAutostart "xdg-desktop-autostart.target"; - After = [ "graphical-session-pre.target" ]; - Before = lib.optional cfg.systemd.xdgAutostart "xdg-desktop-autostart.target"; + systemd.user.targets.mango-session = lib.mkIf cfg.systemd.enable { + Unit = { + Description = "mango compositor session"; + Documentation = [ "man:systemd.special(7)" ]; + BindsTo = [ "graphical-session.target" ]; + Wants = [ + "graphical-session-pre.target" + ] + ++ lib.optional cfg.systemd.xdgAutostart "xdg-desktop-autostart.target"; + After = [ "graphical-session-pre.target" ]; + Before = lib.optional cfg.systemd.xdgAutostart "xdg-desktop-autostart.target"; + }; }; - }; - }; + } + ); }