nix-dotfiles/home/modules/shellAliases.nix

260 lines
8.3 KiB
Nix

{ pkgs, lib, extendedLib, inputs, config, ... }: let
inherit (lib) types mkEnableOption mkOption mdDoc;
cfg = config.local.shell;
# NOTE:
# This module is an over-engineered solution to a non-problem.
# It is a fun experiment in using the Nix language to create a
# shell alias system that organizes aliases into a tree structure,
# with categories and subcategories and subsubcategories and so on.
#
# It also has a lazy join function that will join a list of commands
# with a separator, but render it in a prettier way in a documentation
# file that you can print out and read.
isAlias = v: builtins.isAttrs v && v ? "alias" && v ? "type";
in {
options.local.shell = {
aliases = let
coerceStrToAlias = str: {
type = " ";
alias = [ str ];
};
aliasType = (types.coercedTo types.str coerceStrToAlias (types.submodule {
options = {
type = mkOption {
type = types.enum [ "|" "&&" ";" " " ];
default = ";";
description = ''
If the alias is a list of commands, this is the kind of separator that will be used.
'';
};
alias = mkOption {
type = types.listOf types.str;
};
};
})) // {
# NOTE: this check is necessary, because nix will recurse on types.either,
# and report that the option does not exist.
# See https://discourse.nixos.org/t/problems-with-types-oneof-and-submodules/15197
check = v: builtins.isString v || isAlias v;
};
recursingAliasTreeType = types.attrsOf (types.either aliasType recursingAliasTreeType);
in mkOption {
type = recursingAliasTreeType;
description = "A tree of aliases";
default = { };
example = {
"My first alias category" = {
cmd1 = ''echo "hello world"'';
cmd2 = {
type = "|";
alias = [
"ls -la"
"grep -i hello"
];
};
};
"My second alias category" = {
cmd1 = {
type = "&&";
alias = [
''echo "hello world"''
''echo "goodbye world"''
];
};
};
};
};
variables = mkOption {
type = types.attrsOf types.str;
description = "Environment variables";
default = { };
};
# TODO: I want a similar system for functions at some point.
# functions = {
# };
enablePackageManagerLecture = mkEnableOption "distro reminder messages, aliased to common package manager commands";
enableAliasOverview = mkEnableOption "`aliases` command that prints out a list of all aliases" // {
default = true;
example = false;
};
};
config = let
sedColor =
color:
inputPattern:
outputPattern:
"-e \"s|${inputPattern}|${outputPattern.before or ""}$(tput setaf ${toString color})${outputPattern.middle}$(tput op)${outputPattern.after or ""}|g\"";
colorRed = sedColor 1;
colorSlashes = colorRed "/" {middle = "/";};
# Alias type functors
# These will help pretty print the commands
functors = let
inherit (lib.strings) concatStringsSep;
inherit (extendedLib.termColors.front) blue;
in
{
"|" = {
apply = f: concatStringsSep " | " f.alias;
stringify = f: concatStringsSep (blue "\n| ") f.alias;
};
"&&" = {
apply = f: concatStringsSep " && " f.alias;
stringify = f: concatStringsSep (blue "\n&& ") f.alias;
};
";" = {
apply = f: concatStringsSep "; " f.alias;
stringify = f: concatStringsSep (blue ";\n ") f.alias;
};
" " = {
apply = f: concatStringsSep " " f.alias;
stringify = f: concatStringsSep " \\\n " f.alias;
};
};
aliasTextOverview = let
inherit (lib) stringLength length concatStringsSep replaceStrings
attrValues mapAttrs isAttrs remove replicate mapNullable;
inherit (extendedLib.termColors.front) red green blue;
# String -> String -> String
wrap' = wrapper: str: wrapper + str + wrapper;
# [String] -> String -> String -> String
replaceStrings' = from: to: replaceStrings from (replicate (length from) to);
# String -> Int -> String
repeatString = string: times: concatStringsSep "" (replicate times string);
# int -> String -> AttrSet -> String
stringifyCategory = level: name: category: let
title = "${repeatString " " level}[${green name}]";
commands = attrValues ((lib.flip mapAttrs) category (n: v: let
# String
indent = repeatString " " level;
# String -> String
removeNixLinks = text: let
maybeMatches = builtins.match "(|.*[^)])(/nix/store/.*/bin/).*" text;
matches = mapNullable (remove "") maybeMatches;
in
if (maybeMatches == null)
then text
else replaceStrings' matches "" text;
applyFunctor = attrset: let
applied = functors.${attrset.type}.stringify attrset;
indent' = indent + (repeatString " " ((stringLength " -> \"") + (stringLength n))) + " ";
in
replaceStrings' ["\n"] ("\n" + indent') applied;
recurse = stringifyCategory (level + 1) n v;
in if isAlias v
then "${indent} ${red n} -> ${wrap' (blue "\"") (removeNixLinks (applyFunctor v))}"
else recurse
));
in concatStringsSep "\n" ([title] ++ commands) + "\n";
in (stringifyCategory 0 "Aliases" cfg.aliases) + "\n";
flattenedAliases = let
inherit (lib) mapAttrs attrValues filterAttrs isAttrs
isString concatStringsSep foldr;
applyFunctor = attrset: functors.${attrset.type}.apply attrset;
# TODO: better naming
allAttrValuesAreStrings = attrset: let
# [ {String} ]
filteredAliases = [(filterAttrs (n: v: isString v) attrset)];
# [ {String} ]
remainingFunctors = let
functorSet = filterAttrs (_: v: isAlias v) attrset;
appliedFunctorSet = mapAttrs (n: v: applyFunctor v) functorSet;
in [ appliedFunctorSet ];
# [ {AttrSet} ]
remainingAliasSets = attrValues (filterAttrs (_: v: isAttrs v && !isAlias v) attrset);
# [ {String} ]
recursedAliasSets = filteredAliases
++ (remainingFunctors)
++ (map allAttrValuesAreStrings remainingAliasSets);
in foldr (a: b: a // b) {} recursedAliasSets;
in
allAttrValuesAreStrings cfg.aliases;
in {
xdg.dataFile = {
aliases.text = aliasTextOverview;
packageManagerLecture = lib.mkIf cfg.enablePackageManagerLecture {
target = "package-manager.lecture";
text = let
inherit (extendedLib.termColors.front) red blue;
in lib.concatStringsSep "\n" [
((red "This package manager is not installed on ") + (blue "NixOS") + (red "."))
((red "Either use ") + ("\"nix-env -i\"") + (red " or install it through a configuration file."))
""
];
};
};
local.shell.aliases."Package Managers" = lib.mkIf cfg.enablePackageManagerLecture (let
inherit (lib.attrsets) nameValuePair listToAttrs;
packageManagers = [
"apt"
"dpkg"
"flatpak"
"pacman"
"pamac"
"paru"
"rpm"
"snap"
"xbps"
"yay"
"yum"
];
command = "${pkgs.coreutils}/bin/cat $HOME/${config.xdg.dataFile.packageManagerLecture.target}";
nameValuePairs = map (pm: nameValuePair pm command) packageManagers;
in listToAttrs nameValuePairs);
local.shell.aliases.aliases = lib.mkIf cfg.enableAliasOverview
"${pkgs.coreutils}/bin/cat $HOME/${config.xdg.dataFile.aliases.target}";
programs = {
zsh = {
shellAliases = flattenedAliases;
sessionVariables = cfg.variables;
};
bash = {
shellAliases = flattenedAliases;
sessionVariables = cfg.variables;
};
fish = {
shellAliases = flattenedAliases;
# TODO: fish does not support session variables?
# localVariables = cfg.variables;
};
};
};
}