642 lines
20 KiB
Nix
642 lines
20 KiB
Nix
{
|
|
lib,
|
|
pkgs,
|
|
inputs,
|
|
nixosSystem,
|
|
homeManagerConfiguration,
|
|
system,
|
|
callPackage,
|
|
writeText,
|
|
stdenvNoCC,
|
|
mdbook,
|
|
mdbook-alerts,
|
|
}:
|
|
|
|
let
|
|
nixosConfiguration = nixosSystem {
|
|
inherit system;
|
|
modules = [
|
|
inputs.home-manager.nixosModules.home-manager
|
|
inputs.self.nixosModules.stylix
|
|
./settings.nix
|
|
];
|
|
};
|
|
|
|
homeConfiguration = homeManagerConfiguration {
|
|
inherit pkgs;
|
|
modules = [
|
|
inputs.self.homeModules.stylix
|
|
./settings.nix
|
|
{
|
|
home = {
|
|
homeDirectory = "/home/book";
|
|
stateVersion = "22.11";
|
|
username = "book";
|
|
};
|
|
}
|
|
];
|
|
};
|
|
|
|
# TODO: Include Nix Darwin options
|
|
|
|
platforms = {
|
|
home_manager = {
|
|
name = "Home Manager";
|
|
configuration = homeConfiguration;
|
|
};
|
|
nixos = {
|
|
name = "NixOS";
|
|
configuration = nixosConfiguration;
|
|
};
|
|
};
|
|
|
|
metadata = callPackage "${inputs.self}/stylix/meta.nix" { inherit inputs; };
|
|
|
|
# We construct an index of all Stylix options, using the following format:
|
|
#
|
|
# {
|
|
# "src/options/modules/«module».md" = {
|
|
# referenceSection = "Modules";
|
|
# readme = ''
|
|
# Content of modules/«module»/README.md, or a default title
|
|
# followed by a note about that file not existing.
|
|
#
|
|
# Summary of module maintainers, or a warning that the module
|
|
# is unmaintained.
|
|
# '';
|
|
# optionsByPlatform = {
|
|
# home_manager = [ ... ];
|
|
# nixos = [ ... ];
|
|
# };
|
|
# };
|
|
#
|
|
# "src/options/platforms/«platform».md" = {
|
|
# referenceSection = "Platforms";
|
|
# readme = ''
|
|
# Content of doc/src/options/platforms/«platform».md, or a default
|
|
# title followed by a note about that file not existing.
|
|
# '';
|
|
# optionsByPlatform.«platform» = [ ... ];
|
|
# };
|
|
# }
|
|
#
|
|
# Options are inserted one at a time into the appropriate page, creating
|
|
# new page entries if they don't exist.
|
|
|
|
insert =
|
|
{
|
|
index,
|
|
page,
|
|
emptyPage,
|
|
platform,
|
|
option,
|
|
}:
|
|
index
|
|
// {
|
|
${page} =
|
|
let
|
|
oldPage = index.${page} or emptyPage;
|
|
in
|
|
oldPage
|
|
// {
|
|
optionsByPlatform = oldPage.optionsByPlatform // {
|
|
${platform} = oldPage.optionsByPlatform.${platform} ++ [ option ];
|
|
};
|
|
};
|
|
};
|
|
|
|
insertDeclaration =
|
|
{
|
|
index,
|
|
declaration,
|
|
platform,
|
|
option,
|
|
}:
|
|
# Only include options which are declared by a module within Stylix.
|
|
if lib.hasPrefix "${inputs.self}/" declaration then
|
|
let
|
|
# Part of this string may become an attribute name in the index, and
|
|
# attribute names aren't allowed to have string context. The context
|
|
# comes from `${inputs.self}`, which is removed by `removePrefix`.
|
|
# Therefore, this use of `unsafeDiscardStringContext` is safe.
|
|
pathWithContext = lib.removePrefix "${inputs.self}/" declaration;
|
|
path = builtins.unsafeDiscardStringContext pathWithContext;
|
|
pathComponents = lib.splitString "/" path;
|
|
in
|
|
# Options declared in the modules directory go to the Modules section,
|
|
# otherwise they're assumed to be shared between modules, and go to the
|
|
# Platforms section.
|
|
if builtins.elemAt pathComponents 0 == "modules" then
|
|
let
|
|
module = builtins.elemAt pathComponents 1;
|
|
in
|
|
insert {
|
|
inherit index platform option;
|
|
page = "src/options/modules/${module}.md";
|
|
emptyPage = {
|
|
referenceSection = "Modules";
|
|
|
|
readme =
|
|
let
|
|
maintainers =
|
|
lib.throwIfNot (metadata ? ${module}.maintainers)
|
|
"stylix: ${module} is missing `meta.maintainers`"
|
|
metadata.${module}.maintainers;
|
|
|
|
joinItems =
|
|
items:
|
|
if builtins.length items <= 2 then
|
|
builtins.concatStringsSep " and " items
|
|
else
|
|
builtins.concatStringsSep ", " (
|
|
lib.dropEnd 1 items ++ [ "and ${lib.last items}" ]
|
|
);
|
|
|
|
# Render a maintainer's name and a link to the best contact
|
|
# information we have for them.
|
|
#
|
|
# The reasoning behind the order of preference is as follows:
|
|
#
|
|
# - GitHub:
|
|
# - May link to multiple contact methods
|
|
# - More likely to have up-to-date information than the
|
|
# maintainers list
|
|
# - Protects the email address from crawlers
|
|
# - Email:
|
|
# - Very commonly used
|
|
# - Matrix:
|
|
# - Only other contact method in the schema
|
|
# (as of March 2025)
|
|
# - Name:
|
|
# - If no other information is available, then just show
|
|
# the maintainer's name without a link
|
|
renderMaintainer =
|
|
maintainer:
|
|
if maintainer ? github then
|
|
"[${maintainer.name}](https://github.com/${maintainer.github})"
|
|
else if maintainer ? email then
|
|
"[${maintainer.name}](mailto:${maintainer.email})"
|
|
else if maintainer ? matrix then
|
|
"[${maintainer.name}](https://matrix.to/#/${maintainer.matrix})"
|
|
else
|
|
maintainer.name;
|
|
|
|
renderedMaintainers = joinItems (map renderMaintainer maintainers);
|
|
|
|
ghHandles = toString (
|
|
map (m: lib.optionalString (m ? github) "@${m.github}") maintainers
|
|
);
|
|
|
|
maintainersText = lib.optionalString (
|
|
maintainers != [ ]
|
|
) "**Maintainers**: ${renderedMaintainers} (`${ghHandles}`)";
|
|
|
|
# Render homepages as hyperlinks in readme
|
|
homepage = metadata.${module}.homepage or null;
|
|
|
|
renderedHomepages = joinItems (
|
|
lib.mapAttrsToList (name: url: "[${name}](${url})") homepage
|
|
);
|
|
|
|
homepageText =
|
|
if homepage == null then
|
|
""
|
|
else if builtins.isString homepage then
|
|
"**Homepage**: [${homepage}](${homepage})\n"
|
|
else if builtins.isAttrs homepage then
|
|
lib.throwIf (builtins.length (builtins.attrNames homepage) == 1)
|
|
"stylix: ${module}: `meta.homepage.${builtins.head (builtins.attrNames homepage)}` should be simplified to `meta.homepage`"
|
|
"**Homepages**: ${renderedHomepages}\n"
|
|
else
|
|
throw "stylix: ${module}: unexpected type for `meta.homepage`: ${builtins.typeOf homepage}";
|
|
|
|
name = lib.throwIfNot (
|
|
metadata ? ${module}.name
|
|
) "stylix: ${module} is missing `meta.name`" metadata.${module}.name;
|
|
|
|
in
|
|
lib.concatMapStrings (paragraph: "${paragraph}\n\n") [
|
|
"# ${name}"
|
|
homepageText
|
|
maintainersText
|
|
"---"
|
|
metadata.${module}.description or ""
|
|
];
|
|
|
|
# Module pages initialise all platforms to an empty list, so that
|
|
# '*None provided.*' indicates platforms where the module isn't
|
|
# available.
|
|
optionsByPlatform = lib.mapAttrs (_: _: [ ]) platforms;
|
|
};
|
|
}
|
|
else
|
|
insert {
|
|
inherit index platform option;
|
|
page = "src/options/platforms/${platform}.md";
|
|
emptyPage = {
|
|
referenceSection = "Platforms";
|
|
readme =
|
|
let
|
|
path = "${inputs.self}/doc/src/options/platforms/${platform}.md";
|
|
|
|
# This doesn't count as IFD because ${inputs.self} is a flake input
|
|
mainText =
|
|
if builtins.pathExists path then
|
|
builtins.readFile path
|
|
else
|
|
''
|
|
# ${platform.name}
|
|
> [!NOTE]
|
|
> Documentation is not available for this platform. Its
|
|
> main options are listed below, and you may find more
|
|
> specific options in the documentation for each module.
|
|
'';
|
|
in
|
|
mainText;
|
|
|
|
# Platform pages only initialise that platform, since showing other
|
|
# platforms here would be nonsensical.
|
|
optionsByPlatform.${platform} = [ ];
|
|
};
|
|
}
|
|
else
|
|
index;
|
|
|
|
insertOption =
|
|
{
|
|
index,
|
|
platform,
|
|
option,
|
|
}:
|
|
builtins.foldl' (
|
|
foldIndex: declaration:
|
|
insertDeclaration {
|
|
index = foldIndex;
|
|
inherit declaration platform option;
|
|
}
|
|
) index option.declarations;
|
|
|
|
insertPlatform =
|
|
index: platform:
|
|
builtins.foldl'
|
|
(
|
|
foldIndex: option:
|
|
insertOption {
|
|
index = foldIndex;
|
|
inherit platform option;
|
|
}
|
|
)
|
|
index
|
|
(lib.optionAttrSetToDocList platforms.${platform}.configuration.options);
|
|
|
|
index = builtins.foldl' insertPlatform { } (builtins.attrNames platforms);
|
|
|
|
/**
|
|
Extracts the longest markdown code fence from a string.
|
|
|
|
- `str`: the string to be checked
|
|
- returns: the longest sequence of "`" characters
|
|
*/
|
|
longestFence = longestFence' "";
|
|
|
|
longestFence' =
|
|
prev: str:
|
|
let
|
|
groups = builtins.match "[^`]*(`+)(.*)" str;
|
|
current = builtins.elemAt groups 0;
|
|
remainingStr = builtins.elemAt groups 1;
|
|
prevLen = builtins.stringLength prev;
|
|
currLen = builtins.stringLength current;
|
|
# Reduce to the longest of `prev` & `current`
|
|
longest = if currLen > prevLen then current else prev;
|
|
in
|
|
# If no more matches for "`", return; otherwise keep looking
|
|
if groups == null then prev else longestFence' longest remainingStr;
|
|
|
|
# Renders a value, which should have been created with either lib.literalMD
|
|
# or lib.literalExpression.
|
|
renderValue =
|
|
value:
|
|
if lib.isType "literalMD" value then
|
|
value.text
|
|
else if lib.isType "literalExpression" value then
|
|
let
|
|
# If the text contains ``` characters, our code-fence must be longer
|
|
# than the longest "```"-substring in the text.
|
|
fence = longestFence value.text;
|
|
in
|
|
''
|
|
${fence}```nix
|
|
${value.text}
|
|
${fence}```
|
|
''
|
|
else
|
|
throw "unexpected value type: ${builtins.typeOf value}";
|
|
|
|
# Prefix to remove from file paths when listing where an option is declared.
|
|
declarationPrefix = "${inputs.self}";
|
|
|
|
# Permalink to view a source file on GitHub. If the commit isn't known,
|
|
# then fall back to the latest commit.
|
|
declarationCommit = inputs.self.rev or "master";
|
|
declarationPermalink = "https://github.com/nix-community/stylix/blob/${declarationCommit}";
|
|
|
|
# Renders a single option declaration. Example output:
|
|
#
|
|
# - [modules/module1/nixos.nix](https://github.com/nix-community/stylix/blob/«commit»/modules/module1/nixos.nix)
|
|
renderDeclaration =
|
|
declaration:
|
|
let
|
|
declarationString = toString declaration;
|
|
filePath = lib.removePrefix "${declarationPrefix}/" declarationString;
|
|
in
|
|
if lib.hasPrefix declarationPrefix declarationString then
|
|
"- [${filePath}](${declarationPermalink}/${filePath})"
|
|
else
|
|
throw "declaration not in ${declarationPrefix}: ${declarationString}";
|
|
|
|
# You can embed HTML inside a Markdown document, but to render further
|
|
# Markdown between the HTML tags, it must be surrounded by blank lines:
|
|
# see https://spec.commonmark.org/0.31.2/#html-blocks. This function
|
|
# helps with that.
|
|
#
|
|
# In the following functions, we use concatStrings to build embedded HTML,
|
|
# rather than ${} and multiline strings, because Markdown is sensitive to
|
|
# indentation and may render indented HTML as a code block. The easiest way
|
|
# around this is to generate all the HTML on a single line.
|
|
markdownInHTML = markdown: "\n\n" + markdown + "\n\n";
|
|
|
|
renderDetailsRow =
|
|
name: value:
|
|
lib.concatStrings [
|
|
"<tr>"
|
|
"<td>"
|
|
(markdownInHTML name)
|
|
"</td>"
|
|
"<td>"
|
|
(markdownInHTML value)
|
|
"</td>"
|
|
"</tr>"
|
|
];
|
|
|
|
# Render a single option. Example output (actually HTML, but drawn here using
|
|
# pseudo-Markdown for clarity):
|
|
#
|
|
# ### stylix.option.one
|
|
#
|
|
# The option's description, if present.
|
|
#
|
|
# | Type | string |
|
|
# | Default | The default value, if provided. Usually a code block. |
|
|
# | Example | An example value, if provided. Usually a code block. |
|
|
# | Source | - [modules/module1/nixos.nix](https://github.com/...) |
|
|
renderOption =
|
|
option:
|
|
lib.optionalString (option.visible && !option.internal) ''
|
|
### ${option.name}
|
|
|
|
${option.description or ""}
|
|
|
|
${lib.concatStrings (
|
|
[
|
|
"<table class=\"option-details\">"
|
|
"<colgroup>"
|
|
"<col span=\"1\">"
|
|
"<col span=\"1\">"
|
|
"</colgroup>"
|
|
"<tbody>"
|
|
]
|
|
++ (lib.optional (option ? type) (renderDetailsRow "Type" option.type))
|
|
++ (lib.optional (option ? default) (
|
|
renderDetailsRow "Default" (renderValue option.default)
|
|
))
|
|
++ (lib.optional (option ? example) (
|
|
renderDetailsRow "Example" (renderValue option.example)
|
|
))
|
|
++ (lib.optional (option ? declarations) (
|
|
renderDetailsRow "Source" (
|
|
lib.concatLines (map renderDeclaration option.declarations)
|
|
)
|
|
))
|
|
++ [
|
|
"</tbody>"
|
|
"</table>"
|
|
]
|
|
)}
|
|
'';
|
|
|
|
# Render the list of options for a single platform. Example output:
|
|
#
|
|
# ## NixOS options
|
|
# ### stylix.option.one
|
|
# «option details»
|
|
# ### stylix.option.two
|
|
# «option details»
|
|
renderPlatform =
|
|
platform: options:
|
|
let
|
|
sortedOptions = builtins.sort (a: b: a.name < b.name) options;
|
|
renderedOptions =
|
|
if options == [ ] then
|
|
"*None provided.*"
|
|
else
|
|
lib.concatLines (map renderOption sortedOptions);
|
|
in
|
|
''
|
|
## ${platforms.${platform}.name} options
|
|
${renderedOptions}
|
|
'';
|
|
|
|
# Renders the list of options for all platforms on a page, preceded by either
|
|
# the relevant README, or the default README if it doesn't exist.
|
|
#
|
|
# Example output:
|
|
#
|
|
# # Module 1
|
|
#
|
|
# This is the content of `modules/module1/README.md`, including the title
|
|
# above.
|
|
#
|
|
# ## Home Manager options
|
|
# *None provided.*
|
|
#
|
|
# ## NixOS options
|
|
# «list of options»
|
|
renderPage =
|
|
_path: page:
|
|
let
|
|
options = lib.concatStrings (
|
|
lib.mapAttrsToList renderPlatform page.optionsByPlatform
|
|
);
|
|
in
|
|
lib.concatLines [
|
|
page.readme
|
|
options
|
|
];
|
|
|
|
renderedPages = lib.mapAttrs renderPage index;
|
|
|
|
# SUMMARY.md is generated by a similar method to the main index, using
|
|
# the following format:
|
|
#
|
|
# {
|
|
# Modules = [
|
|
# " - [Module 1](src/options/modules/module1.md)"
|
|
# " - [Module 2](src/options/modules/module2.md)"
|
|
# ];
|
|
# Platforms = [
|
|
# " - [Home Manager](src/options/platforms/home_manager.md)"
|
|
# " - [NixOS](src/options/platforms/nixos.md)"
|
|
# ];
|
|
# }
|
|
#
|
|
# Which renders to the following:
|
|
#
|
|
# - [Modules]()
|
|
# - [Module 1](src/options/modules/module1.md)
|
|
# - [Module 2](src/options/modules/module2.md)
|
|
# - [Platforms]()
|
|
# - [Home Manager](src/options/platforms/home_manager.md)
|
|
# - [NixOS](src/options/platforms/nixos.md)
|
|
#
|
|
# In mdbook, an empty link denotes a draft page, which is used as a parent to
|
|
# collapse the section in the sidebar.
|
|
|
|
insertPageSummary =
|
|
summary: path: page:
|
|
let
|
|
# Extract the title from the first line of the page, and use it in the
|
|
# summary. This ensures that page titles match the sidebar, and ensures
|
|
# that each page begins with a title.
|
|
#
|
|
# TODO: There's potential to use the title from platform pages as the
|
|
# subheading for that platform on other pages, rather than defining a
|
|
# name in the `platforms` attribute set earlier in this file.
|
|
# (This is likely wasted effort unless we have a reason to add a large
|
|
# number of platforms.)
|
|
text = renderedPages.${path};
|
|
lines = lib.splitString "\n" text;
|
|
firstLine = builtins.elemAt lines 0;
|
|
titlePrefix = "# ";
|
|
hasTitle = lib.hasPrefix titlePrefix firstLine;
|
|
title = lib.removePrefix titlePrefix firstLine;
|
|
relativePath = lib.removePrefix "src/" path;
|
|
entry =
|
|
if hasTitle then
|
|
" - [${title}](${relativePath})"
|
|
else
|
|
throw "page must start with a title: ${path}";
|
|
in
|
|
summary
|
|
// {
|
|
${page.referenceSection} = (summary.${page.referenceSection} or [ ]) ++ [
|
|
entry
|
|
];
|
|
};
|
|
|
|
summary = lib.foldlAttrs insertPageSummary { } index;
|
|
|
|
renderSummarySection =
|
|
referenceSection: entries:
|
|
let
|
|
# In mdbook, an empty link denotes a draft page, which is used as a
|
|
# parent so the section can be collapsed in the sidebar.
|
|
parentEntry = "- [${referenceSection}]()";
|
|
in
|
|
[ parentEntry ] ++ entries;
|
|
|
|
renderedSummary = lib.concatLines (
|
|
lib.flatten (lib.mapAttrsToList renderSummarySection summary)
|
|
);
|
|
|
|
# This function generates a Bash script that installs each page to the
|
|
# correct location, over the top of an original copy of doc/src.
|
|
#
|
|
# Each page must be written in a separate derivation, because passing all
|
|
# the text into a single derivation exceeds the maximum size of command
|
|
# line arguments.
|
|
#
|
|
# TODO: It should be possible to use symlinkJoin here, which would make the
|
|
# code more robust at the expense of another intermediate derivation.
|
|
# However, that derivation would be useful during development for inspecting
|
|
# the Markdown before it's rendered to HTML.
|
|
writePages = lib.concatLines (
|
|
lib.mapAttrsToList (
|
|
path: text:
|
|
let
|
|
file = writeText path text;
|
|
in
|
|
"install -D ${file} ${path}"
|
|
) renderedPages
|
|
);
|
|
|
|
# Every option has a separate table containing its details. This CSS makes
|
|
# the following changes for better consistency and compactness:
|
|
#
|
|
# - Fix the width of tables and their columns, so the layout is consistent
|
|
# when scanning through the options. By default, tables are centered and
|
|
# sized to their individual content.
|
|
# - Remove the alternating background colour from rows, which is distracting
|
|
# when there is a small number of rows with a potentially large amount
|
|
# of text per row.
|
|
# - Allow text within a cell to scroll horizontally, which is useful for
|
|
# wide code blocks, especially on mobile devices.
|
|
# - Remove bullet points from lists; this is intended for the list of
|
|
# declarations, as it often contains only one item. Again, this is aimed
|
|
# at mobile devices where horizontal space is limited.
|
|
# TODO: Constrain this rule to only apply to the declarations list, as it
|
|
# may interfere with option descriptions that contain lists.
|
|
extraCSS = ''
|
|
.option-details {
|
|
width: 100%;
|
|
table-layout: fixed;
|
|
}
|
|
.option-details col:first-child {
|
|
width: 7.5em;
|
|
}
|
|
.option-details col:last-child {
|
|
width: 100%;
|
|
overflow-x: auto;
|
|
}
|
|
.option-details tr {
|
|
background: inherit !important;
|
|
}
|
|
.option-details ol, .option-details ul {
|
|
list-style: none;
|
|
padding: unset;
|
|
}
|
|
'';
|
|
|
|
in
|
|
stdenvNoCC.mkDerivation {
|
|
name = "stylix-book";
|
|
src = ./.;
|
|
buildInputs = [
|
|
mdbook
|
|
mdbook-alerts
|
|
];
|
|
|
|
inherit extraCSS renderedSummary;
|
|
passAsFile = [
|
|
"extraCSS"
|
|
"renderedSummary"
|
|
];
|
|
|
|
patchPhase = ''
|
|
${writePages}
|
|
cat $renderedSummaryPath >>src/SUMMARY.md
|
|
cp ${../README.md} src/README.md
|
|
cp ${../gnome.png} src/gnome.png
|
|
cp ${../kde.png} src/kde.png
|
|
'';
|
|
|
|
buildPhase = ''
|
|
runHook preBuild
|
|
mdbook build --dest-dir $out
|
|
runHook postBuild
|
|
'';
|
|
|
|
postBuild = ''
|
|
cat $extraCSSPath >>$out/css/general.css
|
|
'';
|
|
}
|