From c4fa684471d9230f4ca4d4543f02e8ac4c5f749b Mon Sep 17 00:00:00 2001 From: Flameopathic Date: Wed, 21 May 2025 09:39:49 -0400 Subject: [PATCH 1/2] stylix: add mkTarget function Add the mkTarget function, providing a consistent target interface to minimize boilerplate and automatically safeguard declarations related to disabled options. The mkTarget function was first discussed in [1] ("extensive mkTarget function"). [1]: https://github.com/danth/stylix/discussions/1009 --- doc/src/modules.md | 39 ++++++-- stylix/autoload.nix | 39 +++++++- stylix/mk-target.nix | 227 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 294 insertions(+), 11 deletions(-) create mode 100644 stylix/mk-target.nix diff --git a/doc/src/modules.md b/doc/src/modules.md index c7078888..ae29e80c 100644 --- a/doc/src/modules.md +++ b/doc/src/modules.md @@ -41,15 +41,34 @@ folder, using any name which is not on the list above. ## Module template -All modules should have an enable option created using `mkEnableTarget`. This is -similar to -[`mkEnableOption`](https://nix-community.github.io/docnix/reference/lib/options/lib-options-mkenableoption/) -from the standard library, however it integrates with -[`stylix.enable`](./options/nixos.md#stylixenable) and -[`stylix.autoEnable`](./options/nixos.md#stylixautoenable) and generates more -specific documentation. +Modules should be created using the `mkTarget` function whenever possible (see +the [`/stylix/mk-target.nix`]( +https://github.com/danth/stylix/blob/-/stylix/mk-target.nix) in-source +documentation for more details): -A general format for modules is shown below. +```nix +{ config, lib, mkTarget ... }: +mkTarget { + name = "«name»"; + humanName = "«human readable name»"; + + configElements = + { colors }: + { + programs.«name».theme.background = colors.base00; + }; +} +``` + +> [!IMPORTANT] +> The `mkTarget` argument is only available to modules imported by Stylix's +> [autoload system](https://github.com/danth/stylix/blob/-/stylix/autoload.nix), +> e.g., `modules/«target»/«platform».nix` modules. +> +> I.e., it is not available to normal modules imported via the `imports` list. + +When the `mkTarget` function cannot be used, modules must manually replicate its +safeguarding behaviour: ```nix { config, lib, ... }: @@ -66,8 +85,8 @@ A general format for modules is shown below. ``` > [!CAUTION] -> You **must** check _both_ `config.stylix.enable` _and_ your target's own -> `enable` option before defining any config. +> If not using `mkTarget`, you **must** check _both_ `config.stylix.enable` +> _and_ your target's own`enable` option before defining any config. > > In the above example this is done using > `config = lib.mkIf (config.stylix.enable && config.stylix.targets.«name».enable)`. diff --git a/stylix/autoload.nix b/stylix/autoload.nix index 98fb8f5d..5d952e05 100644 --- a/stylix/autoload.nix +++ b/stylix/autoload.nix @@ -8,7 +8,44 @@ builtins.concatLists ( path: kind: let file = "${inputs.self}/modules/${path}/${for}.nix"; + module = import file; + + # Detect whether the file's value has an argument named `mkTarget` + useMkTarget = + builtins.isFunction module && (builtins.functionArgs module) ? mkTarget; + + # NOTE: `mkTarget` cannot be distributed normally through the module system + # due to issues of infinite recursion. + mkTarget = import ./mk-target.nix; in - lib.optional (kind == "directory" && builtins.pathExists file) file + lib.optional (kind == "directory" && builtins.pathExists file) ( + if useMkTarget then + { config, ... }@args: + let + # Based on `lib.modules.applyModuleArgs` + # + # Apply `mkTarget` as a special arg without actually using `specialArgs`, + # which cannot be defined from within a configuration. + context = + name: ''while evaluating the module argument `${name}' in "${toString file}":''; + extraArgs = lib.pipe module [ + builtins.functionArgs + (lib.flip builtins.removeAttrs [ "mkTarget" ]) + (builtins.mapAttrs ( + name: _: + builtins.addErrorContext (context name) ( + args.${name} or config._module.args.${name} + ) + )) + ]; + in + { + key = file; + _file = file; + imports = [ (module (args // extraArgs // { inherit mkTarget; })) ]; + } + else + file + ) ) (builtins.readDir "${inputs.self}/modules") ) diff --git a/stylix/mk-target.nix b/stylix/mk-target.nix new file mode 100644 index 00000000..acd0b5d9 --- /dev/null +++ b/stylix/mk-target.nix @@ -0,0 +1,227 @@ +/** + Provides a consistent target interface, minimizing boilerplate and + automatically safeguarding declarations related to disabled options. + + # Type + + ``` + mkTarget :: AttrSet -> ModuleBody + ``` + + Where `ModuleBody` is a module that doesn't take any arguments. This allows + the caller to use module arguments. + + # Examples + + The `modules/«MODULE»/«PLATFORM».nix` modules should use this function as + follows: + + ```nix + { mkTarget, lib... }: + mkTarget { + name = "«name»"; + humanName = "«human readable name»"; + + generalConfig = + lib.mkIf complexCondition { + home.packages = [ pkgs.hello ]; + }; + + configElements = [ + { programs.«name».theme.name = "stylix"; } + + ( + { colors }: + { + programs.«name».theme.background = colors.base00; + } + ) + + ( + { fonts }: + { + programs.«name».font.name = fonts.monospace.name; + } + ) + ]; + } + ``` + + # Inputs + + `config` (Attribute set) + + : `name` (String) + : The target name used to generate options in the `stylix.targets.${name}` + namespace. + + `humanName` (String) + : The descriptive target name passed to the lib.mkEnableOption function + when generating the `stylix.targets.${name}.enable` option. + + `autoEnable` (Boolean) + : Whether the target should be automatically enabled by default according + to the `stylix.autoEnable` option. + + This should be disabled if manual setup is required or if auto-enabling + causes issues. + + `extraOptions` (Attribute set) + : Additional options to be added in the `stylix.targets.${name}` namespace + along the `stylix.targets.${name}.enable` option. + + For example, an extension guard used in the configuration can be declared + as follows: + ```nix + { extension.enable = lib.mkEnableOption "the bloated dependency"; } + ``` + + `configElements` (List or attribute set or function) + : Configuration functions that are automatically safeguarded when any of + their arguments is disabled. The provided `cfg` argument conveniently + aliases to `config.stylix.targets.${name}`. + + For example, the following configuration is not merged if the stylix + colors option is null: + + ```nix + ( + { colors }: + { + programs.«name».theme.background = colors.base00; + } + ) + ``` + + The `cfg` alias can be accessed as follows: + + ```nix + ( + { cfg }: + { + programs.«name».extension.enable = cfg.extension.enable; + } + ) + ``` + + `generalConfig` (Attribute set or function) + : This argument mirrors the `configElements` argument but intentionally + lacks automatic safeguarding and should only be used for complex + configurations where `configElements` is unsuitable. + + # Environment + + The function is provided alongside module arguments in any modules imported + through `/stylix/autoload.nix`. +*/ + +# TODO: Ideally, in the future, this function returns an actual module by better +# integrating with the /stylix/autoload.nix logic, allowing the following target +# simplification and preventing access to unguarded module arguments by +# requiring /modules//.nix files to be attribute sets instead +# of modules: +# +# { +# name = "example"; +# humanName = "Example Target"; +# +# generalConfig = +# { lib, pkgs }: +# lib.mkIf complexCondition { +# home.packages = [ pkgs.hello ]; +# }; +# +# configElements = [ +# { programs.example.theme.name = "stylix"; } +# +# ( +# { colors }: +# { +# programs.example.theme.background = colors.base00; +# } +# ) +# +# ( +# { fonts }: +# { +# programs.example.font.name = fonts.monospace.name; +# } +# ) +# ]; +# } +{ + name, + humanName, + autoEnable ? true, + extraOptions ? { }, + configElements ? [ ], + generalConfig ? null, +}: +let + module = + { config, lib, ... }: + let + cfg = config.stylix.targets.${name}; + + # Get the list of function de-structured argument names. + functionArgNames = + fn: + lib.pipe fn [ + lib.functionArgs + builtins.attrNames + ]; + + getStylixAttrs = + fn: + lib.genAttrs (functionArgNames fn) ( + arg: + if arg == "cfg" then + cfg + else if arg == "colors" then + config.lib.stylix.colors + else + config.stylix.${arg} + or (throw "stylix: mkTarget expected one of `cfg`, `colors`, ${ + lib.concatMapStringsSep ", " (name: "`${name}`") ( + builtins.attrNames config.stylix + ) + }, but got: ${arg}") + ); + + # Call the configuration function with its required Stylix arguments. + mkConfig = fn: fn (getStylixAttrs fn); + + # Safeguard configuration functions when any of their arguments is + # disabled, while non-function configurations are unguarded. + mkConditionalConfig = + c: + if builtins.isFunction c then + let + allAttrsNonNull = lib.pipe c [ + getStylixAttrs + builtins.attrValues + (builtins.all (attr: attr != null)) + ]; + in + lib.mkIf allAttrsNonNull (mkConfig c) + else + c; + in + { + options.stylix.targets.${name}.enable = + config.lib.stylix.mkEnableTarget humanName autoEnable; + + config = lib.mkIf (config.stylix.enable && cfg.enable) ( + lib.mkMerge ( + lib.optional (generalConfig != null) (mkConfig generalConfig) + ++ map mkConditionalConfig (lib.toList configElements) + ) + ); + }; +in +{ + imports = [ + { options.stylix.targets.${name} = extraOptions; } + module + ]; +} From 7c66eda89ec8eabcdd8f21790224e2971c6f9063 Mon Sep 17 00:00:00 2001 From: Flameopathic Date: Wed, 21 May 2025 09:43:42 -0400 Subject: [PATCH 2/2] treewide: partially apply mkTarget Uses the mkTarget function for Alacritty and Hyprland Home Manager targets --- modules/alacritty/hm.nix | 48 +++++++++++-------- modules/hyprland/hm.nix | 101 ++++++++++++++++++--------------------- 2 files changed, 76 insertions(+), 73 deletions(-) diff --git a/modules/alacritty/hm.nix b/modules/alacritty/hm.nix index 3c529722..a39beb08 100644 --- a/modules/alacritty/hm.nix +++ b/modules/alacritty/hm.nix @@ -1,26 +1,16 @@ # Documentation is available at: # - https://alacritty.org/config-alacritty.html # - `man 5 alacritty` -{ config, lib, ... }: - -let - colors = config.lib.stylix.colors.withHashtag; -in -{ - options.stylix.targets.alacritty.enable = - config.lib.stylix.mkEnableTarget "Alacritty" true; - - config = - lib.mkIf (config.stylix.enable && config.stylix.targets.alacritty.enable) +{ config, mkTarget, ... }: +mkTarget { + name = "alacritty"; + humanName = "Alacritty"; + configElements = [ + ( + { colors }: + with colors.withHashtag; { programs.alacritty.settings = { - font = with config.stylix.fonts; { - normal = { - family = monospace.name; - style = "Regular"; - }; - size = sizes.terminal; - }; window.opacity = config.stylix.opacity.terminal; colors = with colors; { primary = { @@ -60,5 +50,25 @@ in }; }; }; - }; + } + ) + ( + { fonts }: + { + programs.alacritty.settings.font = { + normal = { + family = fonts.monospace.name; + style = "Regular"; + }; + size = fonts.sizes.terminal; + }; + } + ) + ( + { opacity }: + { + programs.alacritty.settings.window.opacity = config.stylix.opacity.terminal; + } + ) + ]; } diff --git a/modules/hyprland/hm.nix b/modules/hyprland/hm.nix index 777f0499..b52dda88 100644 --- a/modules/hyprland/hm.nix +++ b/modules/hyprland/hm.nix @@ -1,59 +1,52 @@ -{ config, lib, ... }: - { - options.stylix.targets.hyprland = { - enable = config.lib.stylix.mkEnableTarget "Hyprland" true; - hyprpaper.enable = config.lib.stylix.mkEnableTarget "Hyprpaper" ( - config.stylix.image != null - ); - }; - - config = - let - cfg = config.stylix.targets.hyprland; - in - lib.mkIf - ( - config.stylix.enable - && cfg.enable - && config.wayland.windowManager.hyprland.enable - ) - ( - lib.mkMerge [ + config, + lib, + mkTarget, + ... +}: +mkTarget { + name = "hyprland"; + humanName = "Hyprland"; + extraOptions.hyprpaper.enable = config.lib.stylix.mkEnableTarget "Hyprpaper" ( + config.stylix.image != null + ); + configElements = [ + ( + { colors }: + { + wayland.windowManager.hyprland.settings = + let + rgb = color: "rgb(${color})"; + rgba = color: alpha: "rgba(${color}${alpha})"; + in { - wayland.windowManager.hyprland.settings = - let - inherit (config.lib.stylix) colors; + decoration.shadow.color = rgba colors.base00 "99"; + general = { + "col.active_border" = rgb colors.base0D; + "col.inactive_border" = rgb colors.base03; + }; + group = { + "col.border_inactive" = rgb colors.base03; + "col.border_active" = rgb colors.base0D; + "col.border_locked_active" = rgb colors.base0C; - rgb = color: "rgb(${color})"; - rgba = color: alpha: "rgba(${color}${alpha})"; - in - { - decoration.shadow.color = rgba colors.base00 "99"; - general = { - "col.active_border" = rgb colors.base0D; - "col.inactive_border" = rgb colors.base03; - }; - group = { - "col.border_inactive" = rgb colors.base03; - "col.border_active" = rgb colors.base0D; - "col.border_locked_active" = rgb colors.base0C; - - groupbar = { - text_color = rgb colors.base05; - "col.active" = rgb colors.base0D; - "col.inactive" = rgb colors.base03; - }; - }; - misc.background_color = rgb colors.base00; + groupbar = { + text_color = rgb colors.base05; + "col.active" = rgb colors.base0D; + "col.inactive" = rgb colors.base03; }; - } - - (lib.mkIf cfg.hyprpaper.enable { - services.hyprpaper.enable = true; - stylix.targets.hyprpaper.enable = true; - wayland.windowManager.hyprland.settings.misc.disable_hyprland_logo = true; - }) - ] - ); + }; + misc.background_color = rgb colors.base00; + }; + } + ) + ( + { cfg }: + (lib.mkIf cfg.hyprpaper.enable { + services.hyprpaper.enable = true; + stylix.targets.hyprpaper.enable = true; + wayland.windowManager.hyprland.settings.misc.disable_hyprland_logo = true; + }) + ) + ]; }