treewide: add and partially apply mkTarget function (#1130)

Add and partially apply the mkTarget function to all appropriate
modules, 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

Link: https://github.com/danth/stylix/pull/1130

Co-authored-by: Daniel Thwaites <danth@danth.me>
Reviewed-by: awwpotato <awwpotato@voidq.com>
Co-authored-by: Matt Sturgeon <matt@sturgeon.me.uk>
Co-authored-by: NAHO <90870942+trueNAHO@users.noreply.github.com>
Reviewed-by: NAHO <90870942+trueNAHO@users.noreply.github.com>
Reviewed-by: Matt Sturgeon <matt@sturgeon.me.uk>
This commit is contained in:
NAHO 2025-05-21 16:21:43 +02:00 committed by GitHub
commit d3fadda72a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 370 additions and 84 deletions

View file

@ -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)`.

View file

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

View file

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

View file

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

227
stylix/mk-target.nix Normal file
View file

@ -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/<MODULE>/<PLATFORM>.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
];
}