{ 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; }; }; }; }