From 258bc85b9ce191bb32d551b1a5391ddb6dad7ba3 Mon Sep 17 00:00:00 2001
From: Robin Stumm <serverkorken@gmail.com>
Date: Mon, 4 Sep 2017 16:39:14 +0200
Subject: [PATCH] zsh: add plugins submodule

To pass compinit security checks,
plugins are liked into ~/zsh/plugins folder.
This also solves issues with a slow start up,
see https://github.com/rycee/home-manager/pull/56#issuecomment-328057513.
---
 modules/misc/news.nix    |  9 ++++
 modules/programs/zsh.nix | 91 +++++++++++++++++++++++++++++++++++++---
 2 files changed, 94 insertions(+), 6 deletions(-)

diff --git a/modules/misc/news.nix b/modules/misc/news.nix
index 3bb9ce80b..9a77dee87 100644
--- a/modules/misc/news.nix
+++ b/modules/misc/news.nix
@@ -107,6 +107,15 @@ in
 
   config = {
     news.entries = [
+      {
+        time = "2017-09-10T22:15:19+00:00";
+        condition = config.programs.zsh.enable;
+        message = ''
+          Home Manager now offers its own minimal zsh plugin manager
+          under the 'programs.zsh.plugins' option path. By statically
+          sourcing your plugins it achieves no startup overhead.
+        '';
+      }
       {
         time = "2017-09-01T10:56:28+00:00";
         message = ''
diff --git a/modules/programs/zsh.nix b/modules/programs/zsh.nix
index be9eb3c73..26de2b449 100644
--- a/modules/programs/zsh.nix
+++ b/modules/programs/zsh.nix
@@ -37,6 +37,36 @@ let
     };
   };
 
+  pluginModule = types.submodule ({ config, ... }: {
+    options = {
+      src = mkOption {
+        type = types.path;
+        description = ''
+          Path to the plugin folder.
+
+          Will be added to <envar>fpath</envar> and <envar>PATH</envar>.
+        '';
+      };
+
+      name = mkOption {
+        type = types.str;
+        description = ''
+          The name of the plugin.
+
+          Don't forget to add <option>file</option>
+          if the script name does not follow convention.
+        '';
+      };
+
+      file = mkOption {
+        type = types.str;
+        description = "The plugin script to source.";
+      };
+    };
+
+    config.file = mkDefault "${config.name}.plugin.zsh";
+  });
+
 in
 
 {
@@ -74,13 +104,43 @@ in
       initExtra = mkOption {
         default = "";
         type = types.lines;
-        description = "Extra commands that should be added to .zshrc.";
+        description = "Extra commands that should be added to <filename>.zshrc</filename>.";
+      };
+
+      plugins = mkOption {
+        type = types.listOf pluginModule;
+        default = [];
+        example = literalExample ''
+          [
+            {
+              # will source zsh-autosuggestions.plugin.zsh
+              name = "zsh-autosuggestions";
+              src = pkgs.fetchFromGitHub {
+                owner = "zsh-users";
+                repo = "zsh-autosuggestions";
+                rev = "v0.4.0";
+                sha256 = "0z6i9wjjklb4lvr7zjhbphibsyx51psv50gm07mbb0kj9058j6kc";
+              };
+            }
+            {
+              name = "enhancd";
+              file = "init.sh";
+              src = pkgs.fetchFromGitHub {
+                owner = "b4b4r07";
+                repo = "enhancd";
+                rev = "v2.2.1";
+                sha256 = "0iqa9j09fwm6nj5rpip87x3hnvbbz9w9ajgm6wkrd5fls8fn8i5g";
+              };
+            }
+          ]
+        '';
+        description = "Plugins to source in <filename>.zshrc</filename>.";
       };
     };
   };
 
-  config = (
-    let
+  config = mkMerge [
+    (let
       aliasesStr = concatStringsSep "\n" (
         mapAttrsToList (k: v: "alias ${k}='${v}'") cfg.shellAliases
       );
@@ -109,15 +169,34 @@ in
 
         HELPDIR="${pkgs.zsh}/share/zsh/$ZSH_VERSION/help"
 
-        ${if cfg.enableCompletion then "autoload -U compinit && compinit" else ""}
+        ${concatStrings (map (plugin: ''
+          path+="$HOME/.zsh/plugins/${plugin.name}"
+          fpath+="$HOME/.zsh/plugins/${plugin.name}"
+        '') cfg.plugins)}
+
+        ${optionalString cfg.enableCompletion "autoload -U compinit && compinit"}
         ${optionalString (cfg.enableAutosuggestions)
           "source ${pkgs.zsh-autosuggestions}/share/zsh-autosuggestions/zsh-autosuggestions.zsh"
         }
 
+        ${concatStrings (map (plugin: ''
+          source "$HOME/.zsh/plugins/${plugin.name}/${plugin.file}"
+        '') cfg.plugins)}
+
         ${aliasesStr}
 
         ${cfg.initExtra}
       '';
-    }
-  );
+    })
+    (mkIf (cfg.plugins != []) {
+      # Many plugins require compinit to be called
+      # but allow the user to opt out.
+      programs.zsh.enableCompletion = mkDefault true;
+
+      home.file = map (plugin: {
+        target = ".zsh/plugins/${plugin.name}";
+        source = plugin.src;
+      }) cfg.plugins;
+    })
+  ];
 }