diff --git a/README.md b/README.md index 741b5b63..e122f884 100644 --- a/README.md +++ b/README.md @@ -75,20 +75,23 @@ When running `nix-shell` or `nix develop`, `mkShell` prints a welcome message: [[general commands]] - hello - prints hello - menu - prints this menu + menu - prints this menu -[formatters] +[packages] - nixpkgs-fmt - Nix code formatter for nixpkgs + diffutils-3.10 - Commands for showing the differences between files (diff, cmp, etc.) + goreleaser-1.23.0 - Deliver Go binaries as fast and easily as possible -[linters] +[scripts] - golangci-lint - golang linter + nix fmt - format Nix files + nix run .#bench - Run benchmark + nix run .#docs - Run mdBook server at http://localhost:3000 [utilites] - hub - github utility + golangci-lint-1.55.2 - golang linter + hub-unstable-2022-12-01 - GitHub utility [devshell]$ ``` diff --git a/docs/src/modules_schema.md b/docs/src/modules_schema.md index 8b9054d3..83c855f7 100644 --- a/docs/src/modules_schema.md +++ b/docs/src/modules_schema.md @@ -1,4 +1,604 @@ -## Options +# Options + +## Available only in `Nix` + +See how `commands.` ([link](https://github.com/numtide/devshell/tree/main/nix/commands/examples.nix)) maps to `commands.*` ([link](https://github.com/numtide/devshell/tree/main/tests/extra/commands.lib.nix)). + +### `commands..*` + +A config for command(s) when the `commands` option is an attrset. + +**Type**: + +```console +(package or string convertible to it) or (list with two elements of types: [ string (package or string convertible to it) ]) or (nestedOptions) or (flatOptions) +``` + +**Example value**: + +```nix +{ + category = [ + { + packages.grep = pkgs.gnugrep; + } + pkgs.python3 + [ "[package] vercel description" "nodePackages.vercel" ] + "nodePackages.yarn" + ]; +} +``` + +**Declared in**: + +- [nix/commands/types.nix](https://github.com/numtide/devshell/tree/main/nix/commands/types.nix) + +### `commands..*.packages (nestedOptions)` + +A leaf value: + +1. When a `string` with a value ``, + devshell tries to resolve a derivation + `pkgs.` and use it as `package (flatOptions)`. + +2. When a `derivation`, it's used as `package (flatOptions)`. + +3. When a list with two elements: + 1. The first element is a `string` + that is used to select `help (flatOptions)`. + + Priority of this `string` (if present) when selecting `help (flatOptions)`: `4`. + + Lowest priority: `1`. + 2. The second element is interpreted as if + the leaf value were initially a `string` or a `derivation`. + +A path to a leaf value is concatenated via `.` +and used as `name (flatOptions)`. + +Priority of `package.meta.description` (if present in the resolved `package (flatOptions)`) +when selecting `help (flatOptions)`: `2` + +Lowest priority: `1`. + +A user may prefer to not bring to the environment some of the packages. + +Priority of `expose = false` when selecting `expose (flatOptions)`: `1`. + +Lowest priority: `1`. + +**Type**: + +```console +(nested (max depth is 100) attribute set of ((package or string convertible to it) or (list with two elements of types: [ string (package or string convertible to it) ]))) +``` + +**Default value**: + +```nix +{ } +``` + +**Example value**: + +```nix +{ + packages.a.b = pkgs.jq; +} +``` + +**Declared in**: + +- [nix/commands/nestedOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/nestedOptions.nix) + +### `commands..*.commands (nestedOptions)` + +A leaf value: + +1. When a `string`, it's used as `command (flatOptions)`. + +2. When a list with two elements: + 1. The first element of type `string` with a value `` + is used to select `help (flatOptions)`. + + Priority of the `` (if present) when selecting `help (flatOptions)`: `4` + + Lowest priority: `1`. + 1. The second element of type `string` is used as `command (flatOptions)`. + +A path to the leaf value is concatenated via `.` +and used as `name (flatOptions)`. + +**Type**: + +```console +(nested (max depth is 100) attribute set of (string or (list with two elements of types: [ string string ]))) +``` + +**Default value**: + +```nix +{ } +``` + +**Declared in**: + +- [nix/commands/nestedOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/nestedOptions.nix) + +### `commands..*.expose (nestedOptions)` + +Can be used as `expose (flatOptions)` for all +`packages (nestedOptions)` and `commands (nestedOptions)`. + +Priority of this option when selecting `expose (flatOptions)`: `2`. + +When selecting `expose (flatOptions)` for +- `package (flatOptions)`, priority of `false`: `1`. +- `command (flatOptions)`, priority of `true`: `1`. + +Lowest priority: `1`. + +**Type**: + +```console +null or boolean +``` + +**Default value**: + +```nix +null +``` + +**Example value**: + +```nix +{ + expose = true; +} +``` + +**Declared in**: + +- [nix/commands/nestedOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/nestedOptions.nix) + +### `commands..*.exposes (nestedOptions)` + +A leaf value can be used as `expose (flatOptions)` +for `package (flatOptions)` or `command (flatOptions)` +with a matching path in `packages (nestedOptions)` or `commands (nestedOptions)`. + +Priority of this option when selecting `expose (flatOptions)`: `3`. + +When selecting `expose (flatOptions)` for +- `package (flatOptions)`, priority of `false`: `1`. +- `command (flatOptions)`, priority of `true`: `1`. + +Lowest priority: `1`. + +**Type**: + +```console +(nested (max depth is 100) attribute set of boolean) +``` + +**Default value**: + +```nix +{ } +``` + +**Example value**: + +```nix +{ + packages.a.b = pkgs.jq; + exposes.a.b = true; +} +``` + +**Declared in**: + +- [nix/commands/nestedOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/nestedOptions.nix) + +### `commands..*.help (nestedOptions)` + +Can be used as `hel (flatOptions)` for all +`packages (nestedOptions)` and `commands (nestedOptions)`. + +Priority of this option when selecting a `help (flatOptions)`: `1`. + +Lowest priority: `1`. + +**Type**: + +```console +string +``` + +**Default value**: + +```nix +"" +``` + +**Example value**: + +```nix +{ + help = "default help"; +} +``` + +**Declared in**: + +- [nix/commands/nestedOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/nestedOptions.nix) + +### `commands..*.helps (nestedOptions)` + +A leaf value can be used as `help (flatOptions)` +for `package (flatOptions)` or `command (flatOptions)` +with a matching path in `packages (nestedOptions)` or `commands (nestedOptions)`. + +Priority of this option when selecting `help (flatOptions)`: `3`. + +Lowest priority: `1`. + +**Type**: + +```console +(nested (max depth is 100) attribute set of string) +``` + +**Default value**: + +```nix +{ } +``` + +**Example value**: + +```nix +{ + packages.a.b = pkgs.jq; + helps.a.b = "run jq"; +} +``` + +**Declared in**: + +- [nix/commands/nestedOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/nestedOptions.nix) + +### `commands..*.interpolate (nestedOptions)` + +When `true`, shell variables in `help (flatOptions)` +can be interpolated. + +Priority of this option when selecting `interpolate (flatOptions)`: `1`. + +Lowest priority: `1`. + +**Type**: + +```console +null or boolean +``` + +**Default value**: + +```nix +null +``` + +**Example value**: + +```nix +true +``` + +**Declared in**: + +- [nix/commands/nestedOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/nestedOptions.nix) + +### `commands..*.interpolates (nestedOptions)` + +A leaf value is used as `interpolate (flatOptions)` +for `package (flatOptions)` or `command (flatOptions)` +with a matching path in `packages (nestedOptions)` or `commands (nestedOptions)`. + +Priority of this option when selecting `interpolate (flatOptions)`: `2`. + +Lowest priority: `1`. + +**Type**: + +```console +(nested (max depth is 100) attribute set of boolean) +``` + +**Default value**: + +```nix +{ } +``` + +**Example value**: + +```nix +true +``` + +**Declared in**: + +- [nix/commands/nestedOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/nestedOptions.nix) + +### `commands..*.prefix (nestedOptions)` + +Can be used as `prefix (flatOptions)` for all +`packages (nestedOptions)` and `commands (nestedOptions)`. + +Priority of this option when selecting a `prefix (flatOptions)`: `1`. + +Lowest priority: `1`. + +**Type**: + +```console +string +``` + +**Default value**: + +```nix +"" +``` + +**Example value**: + +```nix +{ + prefix = "nix run .#"; +} +``` + +**Declared in**: + +- [nix/commands/nestedOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/nestedOptions.nix) + +### `commands..*.prefixes (nestedOptions)` + +A leaf value becomes `prefix (flatOptions)` +of `package (flatOptions)` or `command (flatOptions)` +with a matching path in `packages (nestedOptions)` or `commands (nestedOptions)`. + +Priority of this option when selecting a `prefix (flatOptions)`: `2`. + +Lowest priority: `1`. + +**Type**: + +```console +(nested (max depth is 100) attribute set of string) +``` + +**Default value**: + +```nix +{ } +``` + +**Example value**: + +```nix +{ + packages.a.b = pkgs.jq; + prefixes.a.b = "nix run ../#"; +} +``` + +**Declared in**: + +- [nix/commands/nestedOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/nestedOptions.nix) + +### `commands..*.package (flatOptions)` + +Used to bring in a specific package. This package will be added to the +environment. + +**Type**: + +```console +null or (package or string convertible to it) or package +``` + +**Default value**: + +```nix +null +``` + +**Declared in**: + +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) + +### `commands..*.category (flatOptions)` + +Sets a free text category under which this command is grouped +and shown in the devshell menu. + +**Type**: + +```console +string +``` + +**Default value**: + +```nix +"[general commands]" +``` + +**Declared in**: + +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) + +### `commands..*.command (flatOptions)` + +If defined, it will add a script with the name of the command, and the +content of this value. + +By default it generates a bash script, unless a different shebang is +provided. + +**Type**: + +```console +null or string +``` + +**Default value**: + +```nix +null +``` + +**Example value**: + +```nix +'' + #!/usr/bin/env python + print("Hello") +'' +``` + +**Declared in**: + +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) + +### `commands..*.expose (flatOptions)` + +When `true`, `command (flatOptions)` +or `package (flatOptions)` will be added to the environment. + +Otherwise, they will not be added to the environment, but will be printed +in the devshell menu. + +**Type**: + +```console +boolean +``` + +**Default value**: + +```nix +true +``` + +**Example value**: + +```nix +true +``` + +**Declared in**: + +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) + +### `commands..*.help (flatOptions)` + +Describes what the command does in one line of text. + +**Type**: + +```console +null or string +``` + +**Default value**: + +```nix +null +``` + +**Declared in**: + +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) + +### `commands..*.interpolate (flatOptions)` + +When `true` or when `null` and `devshell.menu.interpolate` is `true`, shell variables in `help (flatOptions)` +will be interpolated. + +Otherwise, they will not. + +**Type**: + +```console +null or boolean +``` + +**Default value**: + +```nix +null +``` + +**Example value**: + +```nix +true +``` + +**Declared in**: + +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) + +### `commands..*.name (flatOptions)` + +Name of the command. + +Defaults to a `package (flatOptions)` name or pname if present. + +The value of this option is required for `command (flatOptions)`. + +**Type**: + +```console +null or string matching [^$ +]+ +``` + +**Default value**: + +```nix +null +``` + +**Declared in**: + +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) + +### `commands..*.prefix (flatOptions)` + +Prefix of the command name in the devshell menu. + +**Type**: + +```console +string +``` + +**Default value**: + +```nix +"" +``` + +**Declared in**: + +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) +## Available in `Nix` and `TOML` ### `commands` @@ -7,7 +607,7 @@ Add commands to the environment. **Type**: ```console -list of (submodule) +(list of ((package or string convertible to it) or (list with two elements of types: [ string (package or string convertible to it) ]) or (flatOptions))) or (attribute set of list of ((package or string convertible to it) or (list with two elements of types: [ string (package or string convertible to it) ]) or (nestedOptions) or (flatOptions))) ``` **Default value**: @@ -19,25 +619,60 @@ list of (submodule) **Example value**: ```nix -[ - { - help = "print hello"; - name = "hello"; - command = "echo hello"; - } +{ + packages = [ + "diffutils" + "goreleaser" + ]; + scripts = [ + { + prefix = "nix run .#"; + inherit packages; + } + { + name = "nix fmt"; + help = "format Nix files"; + } + ]; + utilites = [ + [ "GitHub utility" "gitAndTools.hub" ] + [ "golang linter" "golangci-lint" ] + ]; +} +``` + +**Declared in**: + +- [modules/commands.nix](https://github.com/numtide/devshell/tree/main/modules/commands.nix) + +### `commands.*` + +A config for a command when the `commands` option is a list. + +**Type**: + +```console +(package or string convertible to it) or (list with two elements of types: [ string (package or string convertible to it) ]) or (flatOptions) +``` + +**Example value**: +```nix +[ { - package = "nixpkgs-fmt"; - category = "formatter"; + category = "scripts"; + package = "black"; } + [ "[package] print hello" "hello" ] + "nodePackages.yarn" ] ``` **Declared in**: -- [modules/commands.nix](https://github.com/numtide/devshell/tree/main/modules/commands.nix) +- [nix/commands/types.nix](https://github.com/numtide/devshell/tree/main/nix/commands/types.nix) -### `commands.*.package` +### `commands.*.package (flatOptions)` Used to bring in a specific package. This package will be added to the environment. @@ -45,7 +680,7 @@ environment. **Type**: ```console -null or (package or string convertible to it) +null or (package or string convertible to it) or package ``` **Default value**: @@ -56,12 +691,12 @@ null **Declared in**: -- [modules/commands.nix](https://github.com/numtide/devshell/tree/main/modules/commands.nix) +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) -### `commands.*.category` +### `commands.*.category (flatOptions)` -Set a free text category under which this command is grouped -and shown in the help menu. +Sets a free text category under which this command is grouped +and shown in the devshell menu. **Type**: @@ -77,9 +712,9 @@ string **Declared in**: -- [modules/commands.nix](https://github.com/numtide/devshell/tree/main/modules/commands.nix) +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) -### `commands.*.command` +### `commands.*.command (flatOptions)` If defined, it will add a script with the name of the command, and the content of this value. @@ -110,9 +745,39 @@ null **Declared in**: -- [modules/commands.nix](https://github.com/numtide/devshell/tree/main/modules/commands.nix) +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) + +### `commands.*.expose (flatOptions)` + +When `true`, `command (flatOptions)` +or `package (flatOptions)` will be added to the environment. + +Otherwise, they will not be added to the environment, but will be printed +in the devshell menu. + +**Type**: + +```console +boolean +``` + +**Default value**: + +```nix +true +``` + +**Example value**: + +```nix +true +``` + +**Declared in**: + +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) -### `commands.*.help` +### `commands.*.help (flatOptions)` Describes what the command does in one line of text. @@ -130,16 +795,19 @@ null **Declared in**: -- [modules/commands.nix](https://github.com/numtide/devshell/tree/main/modules/commands.nix) +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) + +### `commands.*.interpolate (flatOptions)` -### `commands.*.name` +When `true` or when `null` and `devshell.menu.interpolate` is `true`, shell variables in `help (flatOptions)` +will be interpolated. -Name of this command. Defaults to attribute name in commands. +Otherwise, they will not. **Type**: ```console -null or string +null or boolean ``` **Default value**: @@ -148,9 +816,60 @@ null or string null ``` +**Example value**: + +```nix +true +``` + **Declared in**: -- [modules/commands.nix](https://github.com/numtide/devshell/tree/main/modules/commands.nix) +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) + +### `commands.*.name (flatOptions)` + +Name of the command. + +Defaults to a `package (flatOptions)` name or pname if present. + +The value of this option is required for `command (flatOptions)`. + +**Type**: + +```console +null or string matching [^$ +]+ +``` + +**Default value**: + +```nix +null +``` + +**Declared in**: + +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) + +### `commands.*.prefix (flatOptions)` + +Prefix of the command name in the devshell menu. + +**Type**: + +```console +string +``` + +**Default value**: + +```nix +"" +``` + +**Declared in**: + +- [nix/commands/flatOptions.nix](https://github.com/numtide/devshell/tree/main/nix/commands/flatOptions.nix) ### `devshell.packages` @@ -255,6 +974,86 @@ true - [modules/devshell.nix](https://github.com/numtide/devshell/tree/main/modules/devshell.nix) +### `devshell.menu` + +Controls devshell menu + +**Type**: + +```console +submodule +``` + +**Default value**: + +```nix +{ } +``` + +**Example value**: + +```nix +{ + interpolate = true; + width = 75; +} +``` + +**Declared in**: + +- [modules/devshell.nix](https://github.com/numtide/devshell/tree/main/modules/devshell.nix) + +### `devshell.menu.interpolate` + +Whether to enable interpolation in the devshell menu. +**Type**: + +```console +boolean +``` + +**Default value**: + +```nix +false +``` + +**Example value**: + +```nix +true +``` + +**Declared in**: + +- [modules/devshell.nix](https://github.com/numtide/devshell/tree/main/modules/devshell.nix) + +### `devshell.menu.width` + +Width of the devshell message. + +**Type**: + +```console +positive integer or floating point number, meaning >0 +``` + +**Default value**: + +```nix +75 +``` + +**Example value**: + +```nix +75 +``` + +**Declared in**: + +- [modules/devshell.nix](https://github.com/numtide/devshell/tree/main/modules/devshell.nix) + ### `devshell.meta` Metadata, such as 'meta.description'. Can be useful as metadata for downstream tooling. @@ -1414,8 +2213,7 @@ true **Declared in**: - [extra/services/postgres.nix](https://github.com/numtide/devshell/tree/main/extra/services/postgres.nix) - -## Extra options +## Extra options available only in `Nix` ### `_module.args` @@ -1471,4 +2269,4 @@ lazy attribute set of raw value **Declared in**: -- [lib/modules.nix]() +- [lib/modules.nix]() \ No newline at end of file diff --git a/flake.nix b/flake.nix index eb28fe0f..a70be7cb 100644 --- a/flake.nix +++ b/flake.nix @@ -38,7 +38,36 @@ }; }; - devShells.default = devshell.fromTOML ./devshell.toml; + devShells = { + default = devshell.mkShell { + bash.extra = '' + export MDBOOK_SERVER_ADDRESS="http://localhost:3000" + ''; + commands = { + packages = [ + "diffutils" # used by golangci-lint + "goreleaser" + ]; + scripts = [ + { + prefix = "nix run .#"; + inherit packages; + helps.docs = ''Run mdBook server at "$MDBOOK_SERVER_ADDRESS"''; + interpolates.docs = true; + } + { + name = "nix fmt"; + help = "format Nix files"; + } + ]; + utilites = [ + [ "GitHub utility" "gitAndTools.hub" ] + [ "golang linter" "golangci-lint" ] + ]; + }; + }; + toml = devshell.fromTOML ./devshell.toml; + }; legacyPackages = import inputs.self { inherit system; @@ -50,7 +79,12 @@ checks = with pkgs.lib; - pipe (import ./tests { inherit pkgs; }) [ + pipe { } [ + (x: + x // (import ./tests { inherit pkgs; }) + // devShells + // { inherit (devshell.modules-docs) markdown; } + ) (collect isDerivation) (map (x: { name = x.name or x.pname; value = x; })) listToAttrs diff --git a/modules/commands.nix b/modules/commands.nix index d4e74e89..377f9fd1 100644 --- a/modules/commands.nix +++ b/modules/commands.nix @@ -1,192 +1,57 @@ -{ lib, config, pkgs, ... }: -with lib; +{ lib, config, pkgs, options, ... }: let - ansi = import ../nix/ansi.nix; - - # Because we want to be able to push pure JSON-like data into the - # environment. - strOrPackage = import ../nix/strOrPackage.nix { inherit lib pkgs; }; - - writeDefaultShellScript = import ../nix/writeDefaultShellScript.nix { - inherit (pkgs) lib writeTextFile bash; - }; - - pad = str: num: - if num > 0 then - pad "${str} " (num - 1) - else - str; - - # Fallback to the package pname if the name is unset - resolveName = cmd: - if cmd.name == null then - cmd.package.pname or (builtins.parseDrvName cmd.package.name).name - else - cmd.name; - - # Fill in default options for a command. - commandToPackage = cmd: - assert lib.assertMsg (cmd.command == null || cmd.name != cmd.command) "[[commands]]: ${toString cmd.name} cannot be set to both the `name` and the `command` attributes. Did you mean to use the `package` attribute?"; - assert lib.assertMsg (cmd.package != null || (cmd.command != null && cmd.command != "")) "[[commands]]: ${resolveName cmd} expected either a command or package attribute."; - if cmd.package == null then - writeDefaultShellScript - { - name = cmd.name; - text = cmd.command; - binPrefix = true; - } - else - cmd.package; - - commandsToMenu = cmds: - let - cleanName = { name, package, ... }@cmd: - assert lib.assertMsg (cmd.name != null || cmd.package != null) "[[commands]]: some command is missing both a `name` or `package` attribute."; - let - name = resolveName cmd; - - help = - if cmd.help == null then - cmd.package.meta.description or "" - else - cmd.help; - in - cmd // { - inherit name help; - }; - - commands = map cleanName cmds; - - commandLengths = - map ({ name, ... }: builtins.stringLength name) commands; - - maxCommandLength = - builtins.foldl' - (max: v: if v > max then v else max) - 0 - commandLengths - ; - - commandCategories = lib.unique ( - (zipAttrsWithNames [ "category" ] (name: vs: vs) commands).category - ); - - commandByCategoriesSorted = - builtins.attrValues (lib.genAttrs - commandCategories - (category: lib.nameValuePair category (builtins.sort - (a: b: a.name < b.name) - (builtins.filter (x: x.category == category) commands) - )) - ); - - opCat = kv: - let - category = kv.name; - cmd = kv.value; - opCmd = { name, help, ... }: - let - len = maxCommandLength - (builtins.stringLength name); - in - if help == null || help == "" then - " ${name}" - else - " ${pad name len} - ${help}"; - in - "\n${ansi.bold}[${category}]${ansi.reset}\n\n" + builtins.concatStringsSep "\n" (map opCmd cmd); - in - builtins.concatStringsSep "\n" (map opCat commandByCategoriesSorted) + "\n"; - - # These are all the options available for the commands. - commandOptions = { - name = mkOption { - type = types.nullOr types.str; - default = null; - description = '' - Name of this command. Defaults to attribute name in commands. - ''; - }; - - category = mkOption { - type = types.str; - default = "[general commands]"; - description = '' - Set a free text category under which this command is grouped - and shown in the help menu. - ''; - }; - - help = mkOption { - type = types.nullOr types.str; - default = null; - description = '' - Describes what the command does in one line of text. - ''; - }; - - command = mkOption { - type = types.nullOr types.str; - default = null; - description = '' - If defined, it will add a script with the name of the command, and the - content of this value. - - By default it generates a bash script, unless a different shebang is - provided. - ''; - example = '' - #!/usr/bin/env python - print("Hello") - ''; - }; - - package = mkOption { - type = types.nullOr strOrPackage; - default = null; - description = '' - Used to bring in a specific package. This package will be added to the - environment. - ''; - }; - }; + inherit (import ../nix/commands/lib.nix { inherit pkgs options config; }) + commandsType + commandToPackage + devshellMenuCommandName + commandsToMenu + ; in { - options.commands = mkOption { - type = types.listOf (types.submodule { options = commandOptions; }); + options.commands = lib.mkOption { + type = commandsType; default = [ ]; description = '' Add commands to the environment. ''; - example = literalExpression '' - [ - { - help = "print hello"; - name = "hello"; - command = "echo hello"; - } - - { - package = "nixpkgs-fmt"; - category = "formatter"; - } - ] + example = lib.literalExpression '' + { + packages = [ + "diffutils" + "goreleaser" + ]; + scripts = [ + { + prefix = "nix run .#"; + inherit packages; + } + { + name = "nix fmt"; + help = "format Nix files"; + } + ]; + utilites = [ + [ "GitHub utility" "gitAndTools.hub" ] + [ "golang linter" "golangci-lint" ] + ]; + } ''; }; config.commands = [ { help = "prints this menu"; - name = "menu"; - command = '' - cat <<'DEVSHELL_MENU' - ${commandsToMenu config.commands} - DEVSHELL_MENU - ''; + name = devshellMenuCommandName; + command = commandsToMenu config.devshell.menu config.commands; } ]; # Add the commands to the devshell packages. Either as wrapper scripts, or # the whole package. - config.devshell.packages = map commandToPackage config.commands; + config.devshell.packages = + lib.filter + (x: x != null) + (map commandToPackage config.commands); + # config.devshell.motd = "$(motd)"; } diff --git a/modules/devshell.nix b/modules/devshell.nix index 2b5186a2..5e6dba17 100644 --- a/modules/devshell.nix +++ b/modules/devshell.nix @@ -12,6 +12,8 @@ let # environment. strOrPackage = import ../nix/strOrPackage.nix { inherit lib pkgs; }; + inherit (import ../nix/commands/lib.nix { inherit pkgs options; }) devshellMenuCommandName; + # Use this to define a flake app for the environment. mkFlakeApp = bin: { type = "app"; @@ -255,7 +257,7 @@ in type = types.str; default = '' {202}🔨 Welcome to ${cfg.name}{reset} - $(type -p menu &>/dev/null && menu) + $(type -p ${devshellMenuCommandName} &>/dev/null && ${devshellMenuCommandName}) ''; apply = replaceStrings (map (key: "{${key}}") (attrNames ansi)) @@ -345,6 +347,30 @@ in options list (except that the 'name' field is ignored). ''; }; + + menu = mkOption { + type = types.submodule { + options.interpolate = mkEnableOption "interpolation in the devshell menu"; + options.width = mkOption { + type = types.numbers.positive; + default = 75; + description = '' + Width of the devshell message. + ''; + example = 75; + }; + }; + default = { }; + description = '' + Controls devshell menu + ''; + example = literalExpression '' + { + interpolate = true; + width = 75; + } + ''; + }; }; config.devshell = { diff --git a/modules/modules-docs.nix b/modules/modules-docs.nix index 547b9f77..598cc5b4 100644 --- a/modules/modules-docs.nix +++ b/modules/modules-docs.nix @@ -47,6 +47,8 @@ let in map (p: repack (unpack p)); + mkUrl = root: path: "${root.url}/tree/${root.branch}/${path}"; + # Transforms a module path into a (path, url) tuple where path is relative # to the repo root, and URL points to an online view of the module. mkDeclaration = @@ -69,7 +71,7 @@ let else rec { path = removePrefix root.prefix decl; - url = "${root.url}/tree/${root.branch}/${path}"; + url = mkUrl root path; }; # Sort modules and put "enable" and "package" declarations first. @@ -80,7 +82,7 @@ let compareWithPrio = pred: cmp: splitByAndCompare pred compare cmp; moduleCmp = compareWithPrio isEnable (compareWithPrio isPackage compare); in - compareLists moduleCmp a.loc b.loc < 0; + compareLists moduleCmp (map toString a.loc) (map toString b.loc) < 0; # Replace functions by the string substFunction = x: @@ -114,14 +116,26 @@ let ) ); + inherit (import ../nix/commands/lib.nix { inherit pkgs options; }) + mkLocSuffix nestedOptionsType flatOptionsType; + # TODO: display values like TOML instead. toMarkdown = optionsDocs: let - optionsDocsPartitioned = partition (opt: head opt.loc != "_module") optionsDocs; + optionsDocsPartitionedIsMain = partition (opt: head opt.loc != "_module") optionsDocs; + nixOnlyLocPrefix = [ "commands" "" ]; + optionsDocsPartitionedIsNixOnly = partition (opt: (take 2 opt.loc) == nixOnlyLocPrefix) optionsDocsPartitionedIsMain.right; + nixOnly = optionsDocsPartitionedIsNixOnly.right; + nixOnlyPartitionedIsTop = partition (opt: opt.loc == nixOnlyLocPrefix ++ [ "*" ]) nixOnly; + nixOnlyPartitionedHasSuffix = partition (opt: ("${last opt.loc}" == "${mkLocSuffix nestedOptionsType.name}")) nixOnlyPartitionedIsTop.wrong; + nixOnlyOrdered = nixOnlyPartitionedIsTop.right ++ nixOnlyPartitionedHasSuffix.right ++ nixOnlyPartitionedHasSuffix.wrong; + nixAndTOMLOrdered = optionsDocsPartitionedIsNixOnly.wrong; + nixExtra = optionsDocsPartitionedIsMain.wrong; + concatOpts = opts: (concatStringsSep "\n\n" (map optToMd opts)); # TODO: handle opt.relatedPackages. What is it for? optToMd = opt: - let heading = lib.showOption opt.loc; in + let heading = lib.showOption (filter isString opt.loc) + concatStrings (filter (x: !(isString x)) opt.loc); in '' ### `${heading}` @@ -165,13 +179,30 @@ let opt.declarations ) ) - + "\n" ; doc = [ - "## Options\n" - (concatStringsSep "\n" (map optToMd optionsDocsPartitioned.right)) - "## Extra options\n" - (concatStringsSep "\n" (map optToMd optionsDocsPartitioned.wrong)) + "# Options\n" + "## Available only in `Nix`\n" + ( + let + root = head cfg.roots; + pathExamples = "nix/commands/examples.nix"; + pathExamplesReal = ../${pathExamples}; + pathCommandsLib = "tests/extra/commands.lib.nix"; + pathCommandsLibReal = ../${pathCommandsLib}; + mkLink = path: "[link](${mkUrl root path})"; + in + assert lib.assertMsg (lib.pathExists pathExamplesReal) "Path `${pathExamplesReal} doesn't exist.`"; + assert lib.assertMsg (lib.pathExists pathCommandsLibReal) "Path `${pathCommandsLibReal} doesn't exist.`"; + '' + See how `commands.` (${mkLink pathExamples}) maps to `commands.*` (${mkLink pathCommandsLib}). + '' + ) + (concatOpts nixOnlyOrdered) + "## Available in `Nix` and `TOML`\n" + (concatOpts nixAndTOMLOrdered) + "## Extra options available only in `Nix`\n" + (concatOpts nixExtra) ]; in concatStringsSep "\n" doc; diff --git a/nix/commands/commandsType.nix b/nix/commands/commandsType.nix new file mode 100644 index 00000000..4feedfb2 --- /dev/null +++ b/nix/commands/commandsType.nix @@ -0,0 +1,159 @@ +{ system ? builtins.currentSystem +, pkgs ? import ../nixpkgs.nix { inherit system; } +, options ? { } +}: +with pkgs.lib; +with builtins; +let + inherit (import ./types.nix { inherit pkgs options; }) + commandsFlatType + commandsNestedType + resolveKey + strOrPackage + ; +in +rec { + mergeDefs = loc: defs: + let + t1 = commandsFlatType; + t2 = commandsNestedType; + defsFlat = t1.merge loc (map (d: d // { value = if isList d.value then d.value else [ ]; }) defs); + defsNested = t2.merge loc (map (d: d // { value = if !(isList d.value) then d.value else { }; }) defs); + in + { inherit defsFlat defsNested; }; + + extractHelp = arg: if isList arg then head arg else null; + + # Fallback to the package pname if the name is unset + resolveName = cmd: + if cmd.name == null then + cmd.package.pname or (parseDrvName cmd.package.name).name + else + cmd.name; + + flattenNonAttrsOrElse = config: alternative: + if !(isAttrs config) || isDerivation config then + let + value = pipe config [ + (x: if isList x then last x else x) + (x: if strOrPackage.check x then resolveKey x else x) + ]; + help = extractHelp config; + in + [{ + name = resolveName value; + inherit help; + ${if isString value then "command" else "package"} = value; + }] + else alternative; + + normalizeCommandsFlat_ = { file ? unknownModule, loc ? [ ], arg ? [ ] }: + pipe arg [ + (value: (mergeDefs loc [{ inherit file value; }]).defsFlat) + (map (config: flattenNonAttrsOrElse config config)) + flatten + (map (value: { inherit file; value = [ value ]; })) + (commandsFlatType.merge loc) + ]; + + highlyUnlikelyAttrName = "adjd-laso-msle-copq-pcod"; + + collectLeaves = attrs: + pipe attrs [ + (mapAttrsRecursiveCond (attrs: !(isDerivation attrs)) + (path: value: { "${highlyUnlikelyAttrName}" = { inherit path; inherit value; }; }) + ) + (collect (hasAttr highlyUnlikelyAttrName)) + (map (x: x.${highlyUnlikelyAttrName})) + ]; + + + normalizeCommandsNested_ = { file ? unknownModule, loc ? [ ], arg ? { } }: + pipe arg [ + # typecheck and augment configs with missing attributes (if a config is an attrset) + (value: (mergeDefs loc [{ inherit file value; }]).defsNested) + (mapAttrsToList + (category: map (config: (map (x: x // { inherit category; })) ( + (flattenNonAttrsOrElse config) ( + # a nestedOptionsType at this point has all attributes due to augmentation + if config?packages then + let + inherit (config) packages commands helps prefixes exposes interpolates; + + mkCommands = forPackages: + pipe (collectLeaves (if forPackages then packages else commands)) [ + (map (leaf: + let + value = pipe leaf.value [ + (x: if isList x then last x else x) + (x: if forPackages && strOrPackage.check x then resolveKey x else x) + ]; + + path = leaf.path; + + name = concatStringsSep "." path; + + help = + if isList leaf.value then + head leaf.value + else + attrByPath path + ( + if isDerivation value then + value.meta.description or null + else config.help or null + ) + helps; + + prefix = attrByPath path config.prefix prefixes; + + expose = attrByPath path + ( + if config.expose != null + then config.expose + else (!forPackages) + ) + exposes; + + interpolate = attrByPath path config.interpolate interpolates; + in + { + "${if forPackages then "package" else "command"}" = value; + inherit name prefix help category expose interpolate; + })) + ]; + in + (mkCommands true) ++ (mkCommands false) + else [ config ] + ) + )))) + flatten + (map (value: { inherit file; value = [ value ]; })) + (commandsFlatType.merge loc) + ]; + + normalizeCommandsNested = arg: normalizeCommandsNested_ { inherit arg; }; + + commandsType = + let + t1 = commandsFlatType; + t2 = commandsNestedType; + either = types.either t1 t2; + in + either // rec { + name = "commandsType"; + description = "(${t1.description}) or (${t2.description})"; + merge = loc: defs: + let + inherit (mergeDefs loc defs) defsFlat defsNested; + defsFlatNormalized = normalizeCommandsFlat_ { arg = defsFlat; inherit loc; }; + defsNestedNormalized = normalizeCommandsNested_ { arg = defsNested; inherit loc; }; + defsMerged = defsFlatNormalized ++ defsNestedNormalized; + in + defsMerged; + getSubOptions = prefix: { + "${t1.name}" = t1.getSubOptions prefix; + "${t2.name}" = t2.getSubOptions prefix; + }; + }; +} diff --git a/nix/commands/devshell.nix b/nix/commands/devshell.nix new file mode 100644 index 00000000..431be554 --- /dev/null +++ b/nix/commands/devshell.nix @@ -0,0 +1,158 @@ +{ system ? builtins.currentSystem +, pkgs ? import ../nixpkgs.nix { inherit system; } +, config ? { } +}: +let + lib = builtins // pkgs.lib; +in +rec { + ansi = import ../ansi.nix; + + writeDefaultShellScript = import ../writeDefaultShellScript.nix { + inherit (pkgs) lib writeTextFile bash; + }; + + devshellMenuCommandName = "menu"; + + pad = str: num: + if num > 0 then + pad "${str} " (num - 1) + else + str; + + resolveName = cmd: + if cmd.name == null then + cmd.package.pname or (lib.parseDrvName cmd.package.name).name + else + cmd.name; + + commandsMessage = "[[commands]]:"; + + # Fill in default options for a command. + commandToPackage = cmd: + if cmd.name != devshellMenuCommandName && cmd.command == null && cmd.package == null then null + else + assert lib.assertMsg (cmd.command == null || cmd.name != cmd.command) "${commandsMessage} in ${lib.generators.toPretty {} cmd}, ${toString cmd.name} cannot be set to both the `name` and the `command` attributes. Did you mean to use the `package` attribute?"; + assert lib.assertMsg ((cmd.package != null && cmd.command == null) || (cmd.command != null && cmd.command != "" && cmd.package == null)) "${commandsMessage} ${lib.generators.toPretty {} cmd} expected either a non-empty command or a package attribute, not both."; + if cmd.package == null + then + writeDefaultShellScript + { + name = cmd.name; + text = cmd.command; + binPrefix = true; + } + else if !cmd.expose + then null + else cmd.package; + + commandsToMenu = menuConfig: cmds: + let + cleanName = { name, package, ... }@cmd: + if + cmd.package == null && (cmd.name != devshellMenuCommandName && cmd.command == null) + && (cmd.prefix != "" || (cmd.name != null && cmd.name != "")) + && cmd.help != null + then + cmd // { + name = "${ + if cmd.prefix != null then cmd.prefix else "" + }${ + if cmd.name != null then cmd.name else "" + }"; + } + else + assert lib.assertMsg (cmd.name != null || cmd.package != null) "${commandsMessage} some command is missing a `name`, a `prefix`, and a `package` attributes."; + let + name = lib.pipe cmd [ + resolveName + (x: if x != null && lib.hasInfix " " x then "'${x}'" else x) + (x: "${cmd.prefix}${x}") + ]; + + help = + if cmd.help == null then + cmd.package.meta.description or "" + else + cmd.help; + in + cmd // { + inherit name help; + }; + + commands = map cleanName cmds; + + commandLengths = + map ({ name, ... }: lib.stringLength name) commands; + + maxCommandLength = + lib.foldl' + (max: v: if v > max then v else max) + 0 + commandLengths + ; + + commandCategories = lib.unique ( + (lib.zipAttrsWithNames [ "category" ] (_: vs: vs) commands).category + ); + + commandByCategoriesSorted = + lib.attrValues (lib.genAttrs + commandCategories + (category: lib.nameValuePair category (lib.sort + (a: b: a.name < b.name) + (lib.filter (x: x.category == category) commands) + )) + ); + + opCat = kv: + let + category = kv.name; + cmd = kv.value; + opCmd = { name, help, interpolate, ... }: + let + len = maxCommandLength - (lib.stringLength name); + + nameWidth = toString maxCommandLength; + helpWidth = toString (config.devshell.menu.width - (maxCommandLength + 5)); + helpHeight = toString 100; + + processHelp = x: + if (if interpolate != null then interpolate else menuConfig.interpolate) + then ''\'' + "\n" + + '' + "$( + cat << EOF + ${x} + EOF + )" + '' + else lib.escapeShellArg x; + + highlyUnlikelyName = "ABDH_OKKD_VOAP_DOEE_PJGD"; + + command = '' + ${highlyUnlikelyName}=${ + if help == null || help == "" + then "" + else processHelp help + } + ${lib.getExe pkgs.perl} ${./scripts/formatCommand.pl} '${toString nameWidth}' '${helpWidth}' '${helpHeight}' '${name}' "''$${highlyUnlikelyName}" + ''; + in + command; + commandsColumns = lib.concatMapStringsSep "\n" opCmd cmd; + in + '' + printf '\n${ansi.bold}[${category}]${ansi.reset}\n\n' + + ${commandsColumns} + ''; + in + '' + { + export LC_ALL="C" + ${lib.concatStringsSep "\n" (map opCat commandByCategoriesSorted) + "\n"} + } + ''; +} diff --git a/nix/commands/examples.nix b/nix/commands/examples.nix new file mode 100644 index 00000000..67b69ab1 --- /dev/null +++ b/nix/commands/examples.nix @@ -0,0 +1,83 @@ +{ system ? builtins.currentSystem +, pkgs ? import ../nixpkgs.nix { inherit system; } +}: +let inherit (pkgs) lib; in +{ + nested = { + "category 1" = [ + { + prefix = "nix run .#"; + prefixes.a.b.yq-1 = "nix run ../#"; + packages = { + a.b = { + jq-1 = [ "[package] jq description" pkgs.jq ]; + yq-1 = pkgs.yq-go; + yq-2 = pkgs.yq-go; + }; + npm = "nodePackages.npm"; + }; + help = "[package] default description"; + helps = { + a.b = { + jq-1 = "[package] another jq description"; + yq-1 = "[package] yq description"; + }; + }; + } + { + packages.a.b = { inherit (pkgs) hyperfine findutils; }; + expose = true; + exposes.a.b.hyperfine = false; + } + { + commands.a.b.awk = ''${lib.getExe pkgs.gawk} $@''; + helps.a.b.awk = "[command] run awk"; + + commands.a.b.jq-2 = [ "[command] run jq" "${lib.getExe pkgs.jq} $@" ]; + + commands."command with spaces" = ''printf "hello\n"''; + helps."command with spaces" = ''[command] print "hello"''; + } + pkgs.python3 + [ "[package] vercel description" "nodePackages.vercel" ] + "nodePackages.yarn" + { + package = pkgs.gnugrep; + } + { + name = "run cowsay"; + help = "run hello"; + package = "cowsay"; + } + { + name = "run perl"; + help = "run perl"; + command = "${lib.getExe pkgs.perl} $@"; + } + { + name = "nix fmt"; + help = "format Nix files"; + } + ]; + category-2 = [ + { + package = pkgs.go; + } + [ "[package] run hello " "hello" ] + pkgs.nixpkgs-fmt + ]; + }; + + flat = [ + { + category = "scripts"; + package = "black"; + } + [ "[package] print hello" "hello" ] + "nodePackages.yarn" + + # uncomment to trigger errors: + # [ "a" ] + # [ "a" "b" "c" ] + ]; +} diff --git a/nix/commands/flatOptions.nix b/nix/commands/flatOptions.nix new file mode 100644 index 00000000..88c3df86 --- /dev/null +++ b/nix/commands/flatOptions.nix @@ -0,0 +1,100 @@ +{ lib, strOrPackage, flatOptionsType, options }: +with lib; +let flat = name: "`${name} (${flatOptionsType.name})`"; in +# These are all the options available for the commands. +{ + prefix = mkOption { + type = types.str; + default = ""; + description = '' + Prefix of the command name in the devshell menu. + ''; + }; + + name = mkOption { + type = types.nullOr (types.str // ( + let regex = "[^$\r\n]+"; in + { + description = "string matching ${regex}"; + check = x: lib.isString x && match regex x != null; + } + )); + default = null; + description = '' + Name of the command. + + Defaults to a ${flat "package"} name or pname if present. + + The value of this option is required for ${flat "command"}. + ''; + }; + + category = mkOption { + type = types.str; + default = "[general commands]"; + description = '' + Sets a free text category under which this command is grouped + and shown in the devshell menu. + ''; + }; + + help = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Describes what the command does in one line of text. + ''; + }; + + command = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + If defined, it will add a script with the name of the command, and the + content of this value. + + By default it generates a bash script, unless a different shebang is + provided. + ''; + example = '' + #!/usr/bin/env python + print("Hello") + ''; + }; + + package = mkOption { + type = types.nullOr (types.oneOf [ strOrPackage types.package ]); + default = null; + description = '' + Used to bring in a specific package. This package will be added to the + environment. + ''; + }; + + expose = mkOption { + type = types.bool; + default = true; + description = '' + When `true`, ${flat "command"} + or ${flat "package"} will be added to the environment. + + Otherwise, they will not be added to the environment, but will be printed + in the devshell menu. + ''; + example = true; + }; + + interpolate = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + When `true` or when `null` and `${ + showOption (options.devshell.menu.type.getSubOptions options.devshell.menu.loc).interpolate.loc + }` is `true`, shell variables in ${flat "help"} + will be interpolated. + + Otherwise, they will not. + ''; + example = true; + }; +} diff --git a/nix/commands/lib.nix b/nix/commands/lib.nix new file mode 100644 index 00000000..8a22e2ac --- /dev/null +++ b/nix/commands/lib.nix @@ -0,0 +1,8 @@ +{ system ? builtins.currentSystem +, pkgs ? import ../nixpkgs.nix { inherit system; } +, options ? { } +, config ? { } +}: +(import ./types.nix { inherit pkgs options; }) // +(import ./devshell.nix { inherit pkgs config; }) // +(import ./commandsType.nix { inherit pkgs options; }) diff --git a/nix/commands/nestedOptions.nix b/nix/commands/nestedOptions.nix new file mode 100644 index 00000000..b1fc00eb --- /dev/null +++ b/nix/commands/nestedOptions.nix @@ -0,0 +1,243 @@ +{ pkgs +, strOrPackage +, attrsNestedOf +, pairHelpPackageType +, pairHelpCommandType +, flatOptionsType +, nestedOptionsType +, maxDepth +}: +with pkgs.lib; +let + flat = name: "`${name} (${flatOptionsType.name})`"; + nested = name: "`${name} (${nestedOptionsType.name})`"; +in +{ + prefix = mkOption { + type = types.str; + default = ""; + description = '' + Can be used as ${flat "prefix"} for all + ${nested "packages"} and ${nested "commands"}. + + Priority of this option when selecting a ${flat "prefix"}: `1`. + + Lowest priority: `1`. + ''; + example = literalExpression '' + { + prefix = "nix run .#"; + } + ''; + }; + + prefixes = mkOption { + type = attrsNestedOf types.str; + default = { }; + description = '' + A leaf value becomes ${flat "prefix"} + of ${flat "package"} or ${flat "command"} + with a matching path in ${nested "packages"} or ${nested "commands"}. + + Priority of this option when selecting a ${flat "prefix"}: `2`. + + Lowest priority: `1`. + ''; + example = literalExpression '' + { + packages.a.b = pkgs.jq; + prefixes.a.b = "nix run ../#"; + } + ''; + }; + + packages = mkOption { + type = + attrsNestedOf ( + types.oneOf [ + strOrPackage + pairHelpPackageType + ] + ); + default = { }; + description = '' + A leaf value: + + 1. When a `string` with a value ``, + devshell tries to resolve a derivation + `pkgs.` and use it as ${flat "package"}. + + 2. When a `derivation`, it's used as ${flat "package"}. + + 3. When a list with two elements: + 1. The first element is a `string` + that is used to select ${flat "help"}. + + Priority of this `string` (if present) when selecting ${flat "help"}: `4`. + + Lowest priority: `1`. + 2. The second element is interpreted as if + the leaf value were initially a `string` or a `derivation`. + + A path to a leaf value is concatenated via `.` + and used as ${flat "name"}. + + Priority of `package.meta.description` (if present in the resolved ${flat "package"}) + when selecting ${flat "help"}: `2` + + Lowest priority: `1`. + + A user may prefer to not bring to the environment some of the packages. + + Priority of `expose = false` when selecting ${flat "expose"}: `1`. + + Lowest priority: `1`. + ''; + example = literalExpression '' + { + packages.a.b = pkgs.jq; + } + ''; + }; + + commands = mkOption { + type = + attrsNestedOf ( + types.oneOf [ + types.str + pairHelpCommandType + ] + ); + default = { }; + description = '' + A leaf value: + + 1. When a `string`, it's used as ${flat "command"}. + + 2. When a list with two elements: + 1. The first element of type `string` with a value `` + is used to select ${flat "help"}. + + Priority of the `` (if present) when selecting ${flat "help"}: `4` + + Lowest priority: `1`. + 1. The second element of type `string` is used as ${flat "command"}. + + A path to the leaf value is concatenated via `.` + and used as ${flat "name"}. + ''; + }; + + help = mkOption { + type = types.str; + default = ""; + description = '' + Can be used as ${flat "hel"} for all + ${nested "packages"} and ${nested "commands"}. + + Priority of this option when selecting a ${flat "help"}: `1`. + + Lowest priority: `1`. + ''; + example = literalExpression '' + { + help = "default help"; + } + ''; + }; + + helps = mkOption { + type = attrsNestedOf types.str; + default = { }; + description = '' + A leaf value can be used as ${flat "help"} + for ${flat "package"} or ${flat "command"} + with a matching path in ${nested "packages"} or ${nested "commands"}. + + Priority of this option when selecting ${flat "help"}: `3`. + + Lowest priority: `1`. + ''; + example = literalExpression '' + { + packages.a.b = pkgs.jq; + helps.a.b = "run jq"; + } + ''; + }; + + expose = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + Can be used as ${flat "expose"} for all + ${nested "packages"} and ${nested "commands"}. + + Priority of this option when selecting ${flat "expose"}: `2`. + + When selecting ${flat "expose"} for + - ${flat "package"}, priority of `false`: `1`. + - ${flat "command"}, priority of `true`: `1`. + + Lowest priority: `1`. + ''; + example = literalExpression '' + { + expose = true; + } + ''; + }; + + exposes = mkOption { + type = attrsNestedOf types.bool; + default = { }; + description = '' + A leaf value can be used as ${flat "expose"} + for ${flat "package"} or ${flat "command"} + with a matching path in ${nested "packages"} or ${nested "commands"}. + + Priority of this option when selecting ${flat "expose"}: `3`. + + When selecting ${flat "expose"} for + - ${flat "package"}, priority of `false`: `1`. + - ${flat "command"}, priority of `true`: `1`. + + Lowest priority: `1`. + ''; + example = literalExpression '' + { + packages.a.b = pkgs.jq; + exposes.a.b = true; + } + ''; + }; + + interpolate = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + When `true`, shell variables in ${flat "help"} + can be interpolated. + + Priority of this option when selecting ${flat "interpolate"}: `1`. + + Lowest priority: `1`. + ''; + example = true; + }; + + interpolates = mkOption { + type = attrsNestedOf types.bool; + default = { }; + description = '' + A leaf value is used as ${flat "interpolate"} + for ${flat "package"} or ${flat "command"} + with a matching path in ${nested "packages"} or ${nested "commands"}. + + Priority of this option when selecting ${flat "interpolate"}: `2`. + + Lowest priority: `1`. + ''; + example = true; + }; +} diff --git a/nix/commands/scripts/formatCommand.pl b/nix/commands/scripts/formatCommand.pl new file mode 100644 index 00000000..c433815d --- /dev/null +++ b/nix/commands/scripts/formatCommand.pl @@ -0,0 +1,29 @@ +use strict; +use warnings; + +my ($nameWidth, $helpWidth, $helpHeight, $name, $help) = @ARGV; + +my $format; +my $delimiter = $help eq "" ? "" : "-"; + +sub getFormat { + my $line1 = " @@{['<' x $nameWidth]}@|^@{['<' x $helpWidth]}"; + my $line2 = "\$name, \$delimiter, \$help"; + my $line3 = "~@{[' ' x ($nameWidth + 4)]}^@{['<' x $helpWidth]}"; + my $line4 = "\$help"; + + $format = <" "*" ]; + declarations = [ "${toString ../..}/nix/commands/types.nix" ]; + }; + }; + }; +} diff --git a/nix/nixpkgs.nix b/nix/nixpkgs.nix index be69a415..731d2bbd 100644 --- a/nix/nixpkgs.nix +++ b/nix/nixpkgs.nix @@ -1,8 +1,10 @@ +{ system ? builtins.currentSystem }: let - # nixpkgs is only used for development. Don't add it to the flake.lock. - gitRev = "2c2a09678ce2ce4125591ac4fe2f7dfaec7a609c"; + lock = builtins.fromJSON (builtins.readFile ../flake.lock); + nixpkgs = + fetchTarball { + url = "https://github.com/NixOS/nixpkgs/archive/${lock.nodes.nixpkgs.locked.rev}.tar.gz"; + sha256 = lock.nodes.nixpkgs.locked.narHash; + }; in -builtins.fetchTarball { - url = "https://github.com/NixOS/nixpkgs/archive/${gitRev}.tar.gz"; - sha256 = "1pkz5bq8f5p9kxkq3142lrrq1592d7zdi75fqzrf02cl1xy2cwvn"; -} +import nixpkgs { inherit system; } diff --git a/nix/strOrPackage.nix b/nix/strOrPackage.nix index 7d49d7d6..ef69930d 100644 --- a/nix/strOrPackage.nix +++ b/nix/strOrPackage.nix @@ -1,12 +1,4 @@ -{ lib, pkgs }: -with lib; -let - resolveKey = key: - let - attrs = builtins.filter builtins.isString (builtins.split "\\." key); - op = sum: attr: sum.${attr} or (throw "package \"${key}\" not found"); - in - builtins.foldl' op pkgs attrs; -in -# Because we want to be able to push pure JSON-like data into the environment. -types.coercedTo types.str resolveKey types.package +{ system ? builtins.currentSystem +, pkgs ? import ./nixpkgs.nix { inherit system; } +, lib ? pkgs.lib +}: (import ./commands/lib.nix { inherit pkgs; }).strOrPackage diff --git a/tests/core/commands.nix b/tests/core/commands.nix index 2376b2f4..026092e3 100644 --- a/tests/core/commands.nix +++ b/tests/core/commands.nix @@ -1,4 +1,5 @@ { pkgs, devshell, runTest }: +let inherit (import ../../nix/commands/lib.nix { inherit pkgs; }) devshellMenuCommandName; in { # Basic devshell usage commands-1 = @@ -33,7 +34,7 @@ # Load the devshell source ${shell}/env.bash - menu + ${devshellMenuCommandName} # Checks that all the commands are available type -p bash-script diff --git a/tests/extra/commands.examples.nix b/tests/extra/commands.examples.nix new file mode 100644 index 00000000..e6c790a9 --- /dev/null +++ b/tests/extra/commands.examples.nix @@ -0,0 +1,52 @@ +{ pkgs, devshell, runTest }: +{ + nested = + let + shell = devshell.mkShell { + devshell.name = "nested-commands-test"; + commands = (import ../../nix/commands/examples.nix { inherit pkgs; }).nested; + }; + in + runTest "nested" { } '' + # Load the devshell + source ${shell}/env.bash + + type -p python3 + + # Has hyperfine + # Has no yq + if [[ -z "$(type -p hyperfine)" ]]; then + echo "OK" + else + echo "Error! Has hyperfine" + fi + + # Has no yq + if [[ -z "$(type -p yq)" ]]; then + echo "OK" + else + echo "Error! Has yq" + fi + ''; + + flat = + let + shell = devshell.mkShell { + devshell.name = "flat-commands-test"; + commands = (import ../../nix/commands/examples.nix { inherit pkgs; }).flat; + }; + in + runTest "flat" { } '' + # Load the devshell + source ${shell}/env.bash + + # Has yarn + type -p yarn + + # Has hello + type -p hello + + # Has black + type -p black + ''; +} diff --git a/tests/extra/commands.lib.nix b/tests/extra/commands.lib.nix new file mode 100644 index 00000000..fa49ed06 --- /dev/null +++ b/tests/extra/commands.lib.nix @@ -0,0 +1,208 @@ +{ pkgs, devshell, runTest }: +let inherit (pkgs) lib; in +{ + normalizeCommandsNested = + let + commands = (import ../../nix/commands/examples.nix { inherit pkgs; }).nested; + normalizedCommands = (import ../../nix/commands/lib.nix { inherit pkgs; }).normalizeCommandsNested commands; + check = normalizedCommands == [ + { + category = "category 1"; + command = null; + expose = false; + help = "[package] jq description"; + interpolate = null; + name = "a.b.jq-1"; + package = pkgs.jq; + prefix = "nix run .#"; + } + { + category = "category 1"; + command = null; + expose = false; + help = "[package] yq description"; + interpolate = null; + name = "a.b.yq-1"; + package = pkgs.yq-go; + prefix = "nix run ../#"; + } + { + category = "category 1"; + command = null; + expose = false; + help = "Portable command-line YAML processor"; + interpolate = null; + name = "a.b.yq-2"; + package = pkgs.yq-go; + prefix = "nix run .#"; + } + { + category = "category 1"; + command = null; + expose = false; + help = "a package manager for JavaScript"; + interpolate = null; + name = "npm"; + package = pkgs.nodePackages.npm; + prefix = "nix run .#"; + } + { + category = "category 1"; + command = null; + expose = true; + help = "GNU Find Utilities, the basic directory searching utilities of the GNU operating system"; + interpolate = null; + name = "a.b.findutils"; + package = pkgs.findutils; + prefix = ""; + } + { + category = "category 1"; + command = null; + expose = false; + help = "Command-line benchmarking tool"; + interpolate = null; + name = "a.b.hyperfine"; + package = pkgs.hyperfine; + prefix = ""; + } + { + category = "category 1"; + command = "${lib.getExe pkgs.gawk} $@"; + expose = true; + help = "[command] run awk"; + interpolate = null; + name = "a.b.awk"; + package = null; + prefix = ""; + } + { + category = "category 1"; + command = "${lib.getExe pkgs.jq} $@"; + expose = true; + help = "[command] run jq"; + interpolate = null; + name = "a.b.jq-2"; + package = null; + prefix = ""; + } + { + category = "category 1"; + command = ''printf "hello\n"''; + expose = true; + help = ''[command] print "hello"''; + interpolate = null; + name = "command with spaces"; + package = null; + prefix = ""; + } + { + category = "category 1"; + command = null; + expose = true; + help = null; + interpolate = null; + name = pkgs.python3.name; + package = pkgs.python3; + prefix = ""; + } + { + category = "category 1"; + command = null; + expose = true; + help = "[package] vercel description"; + interpolate = null; + name = pkgs.nodePackages.vercel.name; + package = pkgs.nodePackages.vercel; + prefix = ""; + } + { + category = "category 1"; + command = null; + expose = true; + help = null; + interpolate = null; + name = pkgs.nodePackages.yarn.name; + package = pkgs.nodePackages.yarn; + prefix = ""; + } + { + category = "category 1"; + command = null; + expose = true; + help = null; + interpolate = null; + name = null; + package = pkgs.gnugrep; + prefix = ""; + } + { + category = "category 1"; + command = null; + expose = true; + help = "run hello"; + interpolate = null; + name = "run cowsay"; + package = pkgs.cowsay; + prefix = ""; + } + { + category = "category 1"; + command = "${lib.getExe pkgs.perl} $@"; + expose = true; + help = "run perl"; + interpolate = null; + name = "run perl"; + package = null; + prefix = ""; + } + { + category = "category 1"; + command = null; + expose = true; + help = "format Nix files"; + interpolate = null; + name = "nix fmt"; + package = null; + prefix = ""; + } + { + category = "category-2"; + command = null; + expose = true; + help = null; + interpolate = null; + name = null; + package = pkgs.go; + prefix = ""; + } + { + category = "category-2"; + command = null; + expose = true; + help = "[package] run hello "; + interpolate = null; + name = pkgs.hello.name; + package = pkgs.hello; + prefix = ""; + } + { + category = "category-2"; + command = null; + expose = true; + help = null; + interpolate = null; + name = pkgs.nixpkgs-fmt.name; + package = pkgs.nixpkgs-fmt; + prefix = ""; + } + ]; + in + runTest "simple" { } '' + ${ + if check + then ''printf "OK"'' + else ''printf "Not OK"; exit 1'' + } + ''; +} diff --git a/tests/extra/devshell.menu.interpolate.nix b/tests/extra/devshell.menu.interpolate.nix new file mode 100644 index 00000000..916cc21a --- /dev/null +++ b/tests/extra/devshell.menu.interpolate.nix @@ -0,0 +1,27 @@ +{ pkgs, devshell, runTest }: +{ + interpolate = + let + shell = devshell.mkShell { + devshell.menu = { + interpolate = true; + width = 200; + }; + commands.scripts = [ + { prefix = "hello"; help = ''hello from "$PRJ_ROOT"!''; } + { prefix = "hola"; help = ''hola from '\$PRJ_ROOT'!''; } + { prefix = "hallo"; help = ''hallo from "$PRJ_ROOT"!''; interpolate = false; } + ]; + }; + in + runTest "interpolate" { } '' + # Check interpolation is enabled + ( eval ${shell}/bin/menu | grep "hello from \"$PRJ_ROOT\"!" ) + + # Check escaped variable + ( eval ${shell}/bin/menu | grep 'hola from '\'''$PRJ_ROOT'\' ) + + # Check non-interpolated variable + ( eval ${shell}/bin/menu | grep 'hallo from "$PRJ_ROOT"!' ) + ''; +}