Skip to content

Commit

Permalink
feat: refactor commands
Browse files Browse the repository at this point in the history
  • Loading branch information
deemp committed Jan 19, 2024
1 parent f213cda commit 7d4e1bb
Show file tree
Hide file tree
Showing 7 changed files with 305 additions and 156 deletions.
159 changes: 6 additions & 153 deletions modules/commands.nix
Original file line number Diff line number Diff line change
@@ -1,163 +1,16 @@
{ lib, config, pkgs, ... }:
with lib;
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/devshell.nix { inherit pkgs; }) commandsToMenu commandToPackage devshellMenuCommandName;
inherit (import ../nix/commands/types.nix { inherit pkgs; }) commandsFlatType;
in
{
options.commands = mkOption {
type = types.listOf (types.submodule { options = commandOptions; });
options.commands = lib.mkOption {
type = commandsFlatType;
default = [ ];
description = ''
Add commands to the environment.
'';
example = literalExpression ''
example = lib.literalExpression ''
[
{
help = "print hello";
Expand All @@ -176,7 +29,7 @@ in
config.commands = [
{
help = "prints this menu";
name = "menu";
name = devshellMenuCommandName;
command = ''
cat <<'DEVSHELL_MENU'
${commandsToMenu config.commands}
Expand Down
4 changes: 3 additions & 1 deletion modules/devshell.nix
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ let
# environment.
strOrPackage = import ../nix/strOrPackage.nix { inherit lib pkgs; };

inherit (import ../nix/commands/devshell.nix { inherit pkgs; }) devshellMenuCommandName;

# Use this to define a flake app for the environment.
mkFlakeApp = bin: {
type = "app";
Expand Down Expand Up @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion modules/modules-docs.nix
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ let

# 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}`
Expand Down
123 changes: 123 additions & 0 deletions nix/commands/devshell.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
{ system ? builtins.currentSystem
, pkgs ? import ../nixpkgs.nix { inherit system; }
}:
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 = 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, ... }:
let
len = maxCommandLength - (lib.stringLength name);
in
if help == null || help == "" then
" ${name}"
else
" ${pad name len} - ${help}";
in
"\n${ansi.bold}[${category}]${ansi.reset}\n\n" + lib.concatStringsSep "\n" (map opCmd cmd);
in
lib.concatStringsSep "\n" (map opCat commandByCategoriesSorted) + "\n";
}
Loading

0 comments on commit 7d4e1bb

Please sign in to comment.