aboutsummaryrefslogtreecommitdiff
path: root/home-manager/modules/programs/fish.nix
blob: 730afa79262ceba5e0594586c7a3f83a50fa29fe (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
{ config, lib, pkgs, ... }:

with lib;

let

  cfg = config.programs.fish;

  pluginModule = types.submodule ({ config, ... }: {
    options = {
      src = mkOption {
        type = types.path;
        description = ''
          Path to the plugin folder.
          </para><para>
          Relevant pieces will be added to the fish function path and
          the completion path. The <filename>init.fish</filename> and
          <filename>key_binding.fish</filename> files are sourced if
          they exist.
        '';
      };

      name = mkOption {
        type = types.str;
        description = ''
          The name of the plugin.
        '';
      };
    };
  });

  functionModule = types.submodule {
    options = {
      body = mkOption {
        type = types.lines;
        description = ''
          The function body.
        '';
      };

      argumentNames = mkOption {
        type = with types; nullOr (either str (listOf str));
        default = null;
        description = ''
          Assigns the value of successive command line arguments to the names
          given.
        '';
      };

      description = mkOption {
        type = with types; nullOr str;
        default = null;
        description = ''
          A description of what the function does, suitable as a completion
          description.
        '';
      };

      wraps = mkOption {
        type = with types; nullOr str;
        default = null;
        description = ''
          Causes the function to inherit completions from the given wrapped
          command.
        '';
      };

      onEvent = mkOption {
        type = with types; nullOr str;
        default = null;
        description = ''
          Tells fish to run this function when the specified named event is
          emitted. Fish internally generates named events e.g. when showing the
          prompt.
        '';
      };

      onVariable = mkOption {
        type = with types; nullOr str;
        default = null;
        description = ''
          Tells fish to run this function when the specified variable changes
          value.
        '';
      };

      onJobExit = mkOption {
        type = with types; nullOr (either str int);
        default = null;
        description = ''
          Tells fish to run this function when the job with the specified group
          ID exits. Instead of a PID, the stringer <literal>caller</literal> can
          be specified. This is only legal when in a command substitution, and
          will result in the handler being triggered by the exit of the job
          which created this command substitution.
        '';
      };

      onProcessExit = mkOption {
        type = with types; nullOr (either str int);
        default = null;
        example = "$fish_pid";
        description = ''
          Tells fish to run this function when the fish child process with the
          specified process ID exits. Instead of a PID, for backwards
          compatibility, <literal>%self</literal> can be specified as an alias
          for <literal>$fish_pid</literal>, and the function will be run when
          the current fish instance exits.
        '';
      };

      onSignal = mkOption {
        type = with types; nullOr (either str int);
        default = null;
        example = [ "SIGHUP" "HUP" 1 ];
        description = ''
          Tells fish to run this function when the specified signal is
          delievered. The signal can be a signal number or signal name.
        '';
      };

      noScopeShadowing = mkOption {
        type = types.bool;
        default = false;
        description = ''
          Allows the function to access the variables of calling functions.
        '';
      };

      inheritVariable = mkOption {
        type = with types; nullOr str;
        default = null;
        description = ''
          Snapshots the value of the specified variable and defines a local
          variable with that same name and value when the function is defined.
        '';
      };
    };
  };

  abbrsStr = concatStringsSep "\n"
    (mapAttrsToList (k: v: "abbr --add --global -- ${k} ${escapeShellArg v}")
      cfg.shellAbbrs);

  aliasesStr = concatStringsSep "\n"
    (mapAttrsToList (k: v: "alias ${k} ${escapeShellArg v}") cfg.shellAliases);

in {
  options = {
    programs.fish = {
      enable = mkEnableOption "fish, the friendly interactive shell";

      package = mkOption {
        type = types.package;
        default = pkgs.fish;
        defaultText = literalExample "pkgs.fish";
        description = ''
          The fish package to install. May be used to change the version.
        '';
      };

      shellAliases = mkOption {
        type = with types; attrsOf str;
        default = { };
        example = literalExample ''
          {
            ll = "ls -l";
            ".." = "cd ..";
          }
        '';
        description = ''
          An attribute set that maps aliases (the top level attribute names
          in this option) to command strings or directly to build outputs.
        '';
      };

      shellAbbrs = mkOption {
        type = with types; attrsOf str;
        default = { };
        example = {
          l = "less";
          gco = "git checkout";
        };
        description = ''
          An attribute set that maps aliases (the top level attribute names
          in this option) to abbreviations. Abbreviations are expanded with
          the longer phrase after they are entered.
        '';
      };

      shellInit = mkOption {
        type = types.lines;
        default = "";
        description = ''
          Shell script code called during fish shell
          initialisation.
        '';
      };

      loginShellInit = mkOption {
        type = types.lines;
        default = "";
        description = ''
          Shell script code called during fish login shell
          initialisation.
        '';
      };

      interactiveShellInit = mkOption {
        type = types.lines;
        default = "";
        description = ''
          Shell script code called during interactive fish shell
          initialisation.
        '';
      };

      promptInit = mkOption {
        type = types.lines;
        default = "";
        description = ''
          Shell script code used to initialise fish prompt.
        '';
      };
    };

    programs.fish.plugins = mkOption {
      type = types.listOf pluginModule;
      default = [ ];
      example = literalExample ''
        [
          {
            name = "z";
            src = pkgs.fetchFromGitHub {
              owner = "jethrokuan";
              repo = "z";
              rev = "ddeb28a7b6a1f0ec6dae40c636e5ca4908ad160a";
              sha256 = "0c5i7sdrsp0q3vbziqzdyqn4fmp235ax4mn4zslrswvn8g3fvdyh";
            };
          }

          # oh-my-fish plugins are stored in their own repositories, which
          # makes them simple to import into home-manager.
          {
            name = "fasd";
            src = pkgs.fetchFromGitHub {
              owner = "oh-my-fish";
              repo = "plugin-fasd";
              rev = "38a5b6b6011106092009549e52249c6d6f501fba";
              sha256 = "06v37hqy5yrv5a6ssd1p3cjd9y3hnp19d3ab7dag56fs1qmgyhbs";
            };
          }
        ]
      '';
      description = ''
        The plugins to source in
        <filename>conf.d/99plugins.fish</filename>.
      '';
    };

    programs.fish.functions = mkOption {
      type = with types; attrsOf (either lines functionModule);
      default = { };
      example = literalExample ''
        {
          __fish_command_not_found_handler = {
            body = "__fish_default_command_not_found_handler $argv[1]";
            onEvent = "fish_command_not_found";
          };

          gitignore = "curl -sL https://www.gitignore.io/api/$argv";
        }
      '';
      description = ''
        Basic functions to add to fish. For more information see
        <link xlink:href="https://fishshell.com/docs/current/cmds/function.html"/>.
      '';
    };

  };

  config = mkIf cfg.enable (mkMerge [
    {
      home.packages = [ cfg.package ];

      xdg.dataFile."fish/home-manager_generated_completions".source = let
        # paths later in the list will overwrite those already linked
        destructiveSymlinkJoin = args_@{ name, paths, preferLocalBuild ? true
          , allowSubstitutes ? false, postBuild ? "", ... }:
          let
            args = removeAttrs args_ [ "name" "postBuild" ] // {
              # pass the defaults
              inherit preferLocalBuild allowSubstitutes;
            };
          in pkgs.runCommand name args ''
            mkdir -p $out
            for i in $paths; do
              if [ -z "$(find $i -prune -empty)" ]; then
                cp -srf $i/* $out
              fi
            done
            ${postBuild}
          '';

        generateCompletions = package:
          pkgs.runCommand "${package.name}-fish-completions" {
            src = package;
            nativeBuildInputs = [ pkgs.python2 ];
            buildInputs = [ cfg.package ];
            preferLocalBuild = true;
            allowSubstitutes = false;
          } ''
            mkdir -p $out
            if [ -d $src/share/man ]; then
              find $src/share/man -type f \
                | xargs python ${cfg.package}/share/fish/tools/create_manpage_completions.py --directory $out \
                > /dev/null
            fi
          '';
      in destructiveSymlinkJoin {
        name = "${config.home.username}-fish-completions";
        paths =
          let cmp = (a: b: (a.meta.priority or 0) > (b.meta.priority or 0));
          in map generateCompletions (sort cmp config.home.packages);
      };

      programs.fish.interactiveShellInit = ''
        # add completions generated by Home Manager to $fish_complete_path
        begin
          set -l joined (string join " " $fish_complete_path)
          set -l prev_joined (string replace --regex "[^\s]*generated_completions.*" "" $joined)
          set -l post_joined (string replace $prev_joined "" $joined)
          set -l prev (string split " " (string trim $prev_joined))
          set -l post (string split " " (string trim $post_joined))
          set fish_complete_path $prev "${config.xdg.dataHome}/fish/home-manager_generated_completions" $post
        end
      '';

      xdg.configFile."fish/config.fish".text = ''
        # ~/.config/fish/config.fish: DO NOT EDIT -- this file has been generated
        # automatically by home-manager.

        # if we haven't sourced the general config, do it
        if not set -q __fish_general_config_sourced

          set -p fish_function_path ${pkgs.fish-foreign-env}/share/fish-foreign-env/functions
          fenv source ${config.home.profileDirectory}/etc/profile.d/hm-session-vars.sh > /dev/null
          set -e fish_function_path[1]

          ${cfg.shellInit}
          # and leave a note so we don't source this config section again from
          # this very shell (children will source the general config anew)
          set -g __fish_general_config_sourced 1

        end

        # if we haven't sourced the login config, do it
        status --is-login; and not set -q __fish_login_config_sourced
        and begin

          # Login shell initialisation
          ${cfg.loginShellInit}

          # and leave a note so we don't source this config section again from
          # this very shell (children will source the general config anew)
          set -g __fish_login_config_sourced 1

        end

        # if we haven't sourced the interactive config, do it
        status --is-interactive; and not set -q __fish_interactive_config_sourced
        and begin

          # Abbreviations
          ${abbrsStr}

          # Aliases
          ${aliasesStr}

          # Prompt initialisation
          ${cfg.promptInit}

          # Interactive shell intialisation
          ${cfg.interactiveShellInit}

          # and leave a note so we don't source this config section again from
          # this very shell (children will source the general config anew,
          # allowing configuration changes in, e.g, aliases, to propagate)
          set -g __fish_interactive_config_sourced 1

        end
      '';
    }
    {
      xdg.configFile = mapAttrs' (name: def: {
        name = "fish/functions/${name}.fish";
        value = {
          text = let
            modifierStr = n: v: optional (v != null) ''--${n}="${toString v}"'';
            modifierStrs = n: v: optional (v != null) "--${n}=${toString v}";
            modifierBool = n: v: optional (v != null && v) "--${n}";

            mods = with def;
              modifierStr "description" description ++ modifierStr "wraps" wraps
              ++ modifierStr "on-event" onEvent
              ++ modifierStr "on-variable" onVariable
              ++ modifierStr "on-job-exit" onJobExit
              ++ modifierStr "on-process-exit" onProcessExit
              ++ modifierStr "on-signal" onSignal
              ++ modifierBool "no-scope-shadowing" noScopeShadowing
              ++ modifierStr "inherit-variable" inheritVariable
              ++ modifierStrs "argument-names" argumentNames;

            modifiers = if isAttrs def then " ${toString mods}" else "";
            body = if isAttrs def then def.body else def;
          in ''
            function ${name}${modifiers}
              ${body}
            end
          '';
        };
      }) cfg.functions;
    }

    # Each plugin gets a corresponding conf.d/plugin-NAME.fish file to load
    # in the paths and any initialization scripts.
    (mkIf (length cfg.plugins > 0) {
      xdg.configFile = mkMerge ((map (plugin: {
        "fish/conf.d/plugin-${plugin.name}.fish".text = ''
          # Plugin ${plugin.name}
          set -l plugin_dir ${plugin.src}

          # Set paths to import plugin components
          if test -d $plugin_dir/functions
            set fish_function_path $fish_function_path[1] $plugin_dir/functions $fish_function_path[2..-1]
          end

          if test -d $plugin_dir/completions
            set fish_complete_path $fish_complete_path[1] $plugin_dir/completions $fish_complete_path[2..-1]
          end

          # Source initialization code if it exists.
          if test -d $plugin_dir/conf.d
            for f in $plugin_dir/conf.d/*.fish
              source $f
            end
          end

          if test -f $plugin_dir/key_bindings.fish
            source $plugin_dir/key_bindings.fish
          end

          if test -f $plugin_dir/init.fish
            source $plugin_dir/init.fish
          end
        '';
      }) cfg.plugins));
    })
  ]);
}